338 lines
9.5 KiB
GDScript
338 lines
9.5 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 `ResourceImporterLayeredTexture`.
|
|
# 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_VIDEO_RAM = 1
|
|
const COMPRESS_UNCOMPRESSED = 2
|
|
# For some reason lossy TextureArrays are not implemented in Godot -_-
|
|
|
|
const COMPRESS_HINT_STRING = "Lossless,VRAM,Uncompressed"
|
|
|
|
const REPEAT_NONE = 0
|
|
const REPEAT_ENABLED = 1
|
|
const REPEAT_MIRRORED = 2
|
|
|
|
const REPEAT_HINT_STRING = "None,Enabled,Mirrored"
|
|
|
|
# TODO COMPRESS_SOURCE_LAYERED is not exposed
|
|
# https://github.com/godotengine/godot/issues/43387
|
|
const Image_COMPRESS_SOURCE_LAYERED = 3
|
|
|
|
|
|
static func import(
|
|
p_source_path: String,
|
|
p_images: Array,
|
|
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) -> Result:
|
|
|
|
var compress_mode := p_compress_mode
|
|
var no_bptc_if_rgb := false#p_options["compress/no_bptc_if_rgb"];
|
|
var repeat := p_repeat
|
|
var filter := p_filter
|
|
var mipmaps := p_mipmaps
|
|
var srgb := 1 if p_contains_albedo else 2#p_options["flags/srgb"];
|
|
# int hslices = p_options["slices/horizontal"];
|
|
# int vslices = p_options["slices/vertical"];
|
|
|
|
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 srgb == 1:
|
|
tex_flags |= Texture.FLAG_CONVERT_TO_LINEAR
|
|
|
|
# Vector<Ref<Image> > slices;
|
|
#
|
|
# int slice_w = image->get_width() / hslices;
|
|
# int slice_h = image->get_height() / vslices;
|
|
|
|
# Can't do any of this in our case...
|
|
#optimize
|
|
# if compress_mode == COMPRESS_VIDEO_RAM:
|
|
#if using video ram, optimize
|
|
# if srgb:
|
|
#remove alpha if not needed, so compression is more efficient
|
|
# if image.get_format() == Image.FORMAT_RGBA8 and !image.detect_alpha():
|
|
# image.convert(Image.FORMAT_RGB8)
|
|
|
|
# else:
|
|
# pass
|
|
# Not exposed to GDScript...
|
|
#image.optimize_channels()
|
|
|
|
var extension := "texarr"
|
|
var formats_imported := []
|
|
|
|
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 encode_bptc := false
|
|
|
|
if ProjectSettings.get("rendering/vram_compression/import_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 encode_bptc := true
|
|
# if no_bptc_if_rgb:
|
|
# var channels := image.get_detected_channels()
|
|
# if channels != Image.DETECTED_LA and channels != Image.DETECTED_RGBA:
|
|
# encode_bptc = false
|
|
|
|
formats_imported.push_back("bptc");
|
|
|
|
if encode_bptc:
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + ".bptc." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_BPTC,
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
r_platform_variants.push_back("bptc")
|
|
ok_on_pc = true
|
|
|
|
if ProjectSettings.get("rendering/vram_compression/import_s3tc"):
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + ".s3tc." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_S3TC,
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
r_platform_variants.push_back("s3tc")
|
|
ok_on_pc = true
|
|
formats_imported.push_back("s3tc")
|
|
|
|
if ProjectSettings.get("rendering/vram_compression/import_etc2"):
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + ".etc2." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_ETC2,
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
r_platform_variants.push_back("etc2")
|
|
formats_imported.push_back("etc2")
|
|
|
|
if ProjectSettings.get("rendering/vram_compression/import_etc"):
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + ".etc." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_ETC,
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
r_platform_variants.push_back("etc")
|
|
formats_imported.push_back("etc")
|
|
|
|
if ProjectSettings.get("rendering/vram_compression/import_pvrtc"):
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + ".pvrtc." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_PVRTC4,
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
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
|
|
var result = _save_tex(
|
|
p_images,
|
|
p_save_path + "." + extension,
|
|
compress_mode,
|
|
Image.COMPRESS_S3TC, #this is ignored
|
|
mipmaps,
|
|
tex_flags)
|
|
|
|
if not result.success:
|
|
return result
|
|
|
|
# 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)
|
|
|
|
|
|
# The input image can be modified
|
|
static func _save_tex(
|
|
p_images: Array,
|
|
p_to_path: String,
|
|
p_compress_mode: int,
|
|
p_vram_compression: int, # Image.CompressMode
|
|
p_mipmaps: bool,
|
|
p_texture_flags: int
|
|
) -> Result:
|
|
|
|
# We only do TextureArrays for now
|
|
var is_3d = false
|
|
|
|
var f := File.new()
|
|
var err := f.open(p_to_path, File.WRITE)
|
|
f.store_8(ord('G'))
|
|
f.store_8(ord('D'))
|
|
if is_3d:
|
|
f.store_8(ord('3'))
|
|
else:
|
|
f.store_8(ord('A'))
|
|
f.store_8(ord('T')) # godot streamable texture
|
|
|
|
var slice_count := len(p_images)
|
|
|
|
f.store_32(p_images[0].get_width())
|
|
f.store_32(p_images[0].get_height())
|
|
f.store_32(slice_count) # depth
|
|
f.store_32(p_texture_flags)
|
|
var image_format : int = p_images[0].get_format()
|
|
var image_size : Vector2 = p_images[0].get_size()
|
|
if p_compress_mode != COMPRESS_VIDEO_RAM:
|
|
# vram needs to do a first compression to tell what the format is, for the rest its ok
|
|
f.store_32(image_format)
|
|
f.store_32(p_compress_mode) # 0 - lossless (PNG), 1 - vram, 2 - uncompressed
|
|
|
|
if (p_compress_mode == COMPRESS_LOSSLESS) and image_format > Image.FORMAT_RGBA8:
|
|
p_compress_mode = COMPRESS_UNCOMPRESSED # these can't go as lossy
|
|
|
|
for i in slice_count:
|
|
var image : Image = p_images[i]
|
|
|
|
if image.get_format() != image_format:
|
|
return Result.new(false, "Layer {0} has different format, got {1}, expected {2}" \
|
|
.format([i, image.get_format(), image_format])).with_value(ERR_INVALID_DATA)
|
|
|
|
if image.get_size() != image_size:
|
|
return Result.new(false, "Layer {0} has different size, got {1}, expected {2}" \
|
|
.format([i, image.get_size(), image_size])).with_value(ERR_INVALID_DATA)
|
|
|
|
# We need to operate on a copy,
|
|
# because the calling code can invoke the function multiple times
|
|
image = image.duplicate()
|
|
|
|
match p_compress_mode:
|
|
COMPRESS_LOSSLESS:
|
|
# We save each mip as PNG so we dont need to do that.
|
|
# The engine code does it anyways :shrug: (see below why...)
|
|
# var image = p_images[i].duplicate()
|
|
# if p_mipmaps:
|
|
# image.generate_mipmaps()
|
|
# else:
|
|
# image.clear_mipmaps()
|
|
|
|
var mmc := _get_required_mipmap_count(image)
|
|
f.store_32(mmc)
|
|
|
|
for j in mmc:
|
|
if j > 0:
|
|
# TODO This function does something fishy behind the scenes:
|
|
# It assumes mipmaps are downscaled versions of the image.
|
|
# This is not necessarily true.
|
|
# See https://www.kotaku.com.au/2018/03/
|
|
# how-nintendo-did-the-water-effects-in-super-mario-sunshine/
|
|
image.shrink_x2()
|
|
|
|
#var data = Image::lossless_packer(image);
|
|
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_VIDEO_RAM:
|
|
# var image : Image = p_images[i]->duplicate();
|
|
image.generate_mipmaps(false)
|
|
|
|
var csource := Image_COMPRESS_SOURCE_LAYERED
|
|
image.compress(p_vram_compression, csource, 0.7)
|
|
|
|
if i == 0:
|
|
#hack so we can properly tell the format
|
|
f.store_32(image.get_format())
|
|
f.store_32(p_compress_mode); # 0 - lossless (PNG), 1 - vram, 2 - uncompressed
|
|
|
|
var data := image.get_data()
|
|
f.store_buffer(data)
|
|
|
|
COMPRESS_UNCOMPRESSED:
|
|
# Ref<Image> image = p_images[i]->duplicate();
|
|
|
|
if p_mipmaps:
|
|
image.generate_mipmaps()
|
|
else:
|
|
image.clear_mipmaps()
|
|
|
|
var data := image.get_data()
|
|
f.store_buffer(data)
|
|
|
|
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)
|