434 lines
12 KiB
GDScript
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)
|
|
|
|
|