bec4114bea
This change makes it possible to use Crunch algorithms for ETC textures with Alpha channel.
Explanation:
For simplicity, Crunch algorithms currently do not use ETC2 specific modes (T, H or P). For this reason, the currently used ETC2A compression format is technically equivalent to ETC1 + Alpha. Note that ETC2 encoding is a superset of ETC1, so any texture, which consists of ETC1 color blocks and ETC2 Alpha blocks, can be correctly decoded by an ETC2A (ETC2_RGBA8) decoder.
Compression scheme for ETC2 Alpha blocks is equivalent to the compression scheme for DXT5 Alpha blocks. ETC2 Alpha endpoint clusterization is performed based on the very same output of the Alpha palettizer which is used for DXT5 Alpha. The only part which is actually different is the Alpha endpoint optimization step.
In order to perform ETC2 Alpha encoding, we can first run the already existing algorithm for DXT5 Alpha endpoint optimization, in order to obtain the initial approximate solution. Then the approximate solution is refined based on the ETC2 Alpha modifier table. When performing raw ETC2A encoding, all the 16 ETC2 Alpha modifiers are used during optimization. However, when performing ETC2A quantization, for performance reasons, only 2 Alpha modifiers are currently used (modifier 13, which allows to perform precise approximation on short Alpha intervals, and modifier 11, which has more or less regularly distributed values, and is used for large Alpha intervals).
For compatibility reasons, ETC2 color compression wrappers have also been added to the code, though, as has been mentioned before, at the current moment ETC2 specific modes are not used, so ETC2 color compression is currently equivalent to ETC1 compression.
The ETC decoder functionality has been significantly extended, Crunch is now capable to decode ETC2 and ETC2A textures (input ETC2 textures can have T, H or P blocks).
In order to use ETC2A compression, use the -ETC2A command line option (i.e. "crunch_x64.exe -ETC2A input.png"). By default, compressed ETC2A textures will be decompressed into KTX file format.
DXT Testing:
The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). All the decompressed test images are identical to the images being compressed and decompressed using original version of Crunch (revision ea9b8d8).
[Compressing Kodak set without mipmaps using DXT1 encoding]
Original: 1582222 bytes / 28.880 sec
Modified: 1468204 bytes / 13.288 sec
Improvement: 7.21% (compression ratio) / 53.99% (compression time)
[Compressing Kodak set with mipmaps using DXT1 encoding]
Original: 2065243 bytes / 36.936 sec
Modified: 1914805 bytes / 18.044 sec
Improvement: 7.28% (compression ratio) / 51.15% (compression time)
ETC Testing:
The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). The ETC1 quantization parameters have been selected in such a way, so that ETC1 compression gives approximately the same average Luma PSNR as the corresponding DXT1 compression (which is equal to 34.044 dB for the Kodak test set compressed without mipmaps using DXT1 encoding and default quality settings).
[Compressing Kodak set without mipmaps using ETC1 encoding]
Total size: 1607858 bytes
Total time: 17.361 sec
Average bitrate: 1.363 bpp
Average Luma PSNR: 34.050 dB
664 lines
28 KiB
C++
664 lines
28 KiB
C++
// File: crn_texture_conversion.cpp
|
|
// See Copyright Notice and license at the end of inc/crnlib.h
|
|
#include "crn_core.h"
|
|
#include "crn_texture_conversion.h"
|
|
#include "crn_console.h"
|
|
#include "crn_file_utils.h"
|
|
#include "crn_cfile_stream.h"
|
|
#include "crn_image_utils.h"
|
|
#include "crn_texture_comp.h"
|
|
#include "crn_strutils.h"
|
|
|
|
namespace crnlib {
|
|
namespace texture_conversion {
|
|
struct progress_params {
|
|
convert_params* m_pParams;
|
|
uint m_start_percentage;
|
|
bool m_canceled;
|
|
};
|
|
|
|
convert_stats::convert_stats() {
|
|
clear();
|
|
}
|
|
|
|
bool convert_stats::init(
|
|
const char* pSrc_filename,
|
|
const char* pDst_filename,
|
|
mipmapped_texture& src_tex,
|
|
texture_file_types::format dst_file_type,
|
|
bool lzma_stats) {
|
|
m_src_filename = pSrc_filename;
|
|
m_dst_filename = pDst_filename;
|
|
m_dst_file_type = dst_file_type;
|
|
|
|
m_pInput_tex = &src_tex;
|
|
|
|
file_utils::get_file_size(pSrc_filename, m_input_file_size);
|
|
file_utils::get_file_size(pDst_filename, m_output_file_size);
|
|
|
|
m_total_input_pixels = 0;
|
|
for (uint i = 0; i < src_tex.get_num_levels(); i++) {
|
|
uint width = math::maximum<uint>(1, src_tex.get_width() >> i);
|
|
uint height = math::maximum<uint>(1, src_tex.get_height() >> i);
|
|
m_total_input_pixels += width * height * src_tex.get_num_faces();
|
|
}
|
|
|
|
m_output_comp_file_size = 0;
|
|
|
|
m_total_output_pixels = 0;
|
|
|
|
if (lzma_stats) {
|
|
vector<uint8> dst_tex_bytes;
|
|
if (!cfile_stream::read_file_into_array(pDst_filename, dst_tex_bytes)) {
|
|
console::error("Failed loading output file: %s", pDst_filename);
|
|
return false;
|
|
}
|
|
if (!dst_tex_bytes.size()) {
|
|
console::error("Output file is empty: %s", pDst_filename);
|
|
return false;
|
|
}
|
|
vector<uint8> cmp_tex_bytes;
|
|
lzma_codec lossless_codec;
|
|
if (lossless_codec.pack(dst_tex_bytes.get_ptr(), dst_tex_bytes.size(), cmp_tex_bytes)) {
|
|
m_output_comp_file_size = cmp_tex_bytes.size();
|
|
}
|
|
}
|
|
|
|
if (!m_output_tex.read_from_file(pDst_filename, m_dst_file_type)) {
|
|
console::error("Failed loading output file: %s", pDst_filename);
|
|
return false;
|
|
}
|
|
|
|
for (uint i = 0; i < m_output_tex.get_num_levels(); i++) {
|
|
uint width = math::maximum<uint>(1, m_output_tex.get_width() >> i);
|
|
uint height = math::maximum<uint>(1, m_output_tex.get_height() >> i);
|
|
m_total_output_pixels += width * height * m_output_tex.get_num_faces();
|
|
}
|
|
CRNLIB_ASSERT(m_total_output_pixels == m_output_tex.get_total_pixels_in_all_faces_and_mips());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool convert_stats::print(bool psnr_metrics, bool mip_stats, bool grayscale_sampling, const char* pCSVStatsFile) const {
|
|
if (!m_pInput_tex)
|
|
return false;
|
|
|
|
console::info("Input texture: %ux%u, Levels: %u, Faces: %u, Format: %s",
|
|
m_pInput_tex->get_width(),
|
|
m_pInput_tex->get_height(),
|
|
m_pInput_tex->get_num_levels(),
|
|
m_pInput_tex->get_num_faces(),
|
|
pixel_format_helpers::get_pixel_format_string(m_pInput_tex->get_format()));
|
|
|
|
// Just casting the uint64's filesizes to uint32 here to work around gcc issues - it's not even possible to have files that large anyway.
|
|
console::info("Input pixels: %u, Input file size: %u, Input bits/pixel: %1.3f",
|
|
m_total_input_pixels, (uint32)m_input_file_size, (m_input_file_size * 8.0f) / m_total_input_pixels);
|
|
|
|
console::info("Output texture: %ux%u, Levels: %u, Faces: %u, Format: %s",
|
|
m_output_tex.get_width(),
|
|
m_output_tex.get_height(),
|
|
m_output_tex.get_num_levels(),
|
|
m_output_tex.get_num_faces(),
|
|
pixel_format_helpers::get_pixel_format_string(m_output_tex.get_format()));
|
|
|
|
console::info("Output pixels: %u, Output file size: %u, Output bits/pixel: %1.3f",
|
|
m_total_output_pixels, (uint32)m_output_file_size, (m_output_file_size * 8.0f) / m_total_output_pixels);
|
|
|
|
if (m_output_comp_file_size) {
|
|
console::info("LZMA compressed output file size: %u bytes, %1.3f bits/pixel",
|
|
(uint32)m_output_comp_file_size, (m_output_comp_file_size * 8.0f) / m_total_output_pixels);
|
|
}
|
|
if (psnr_metrics) {
|
|
if ((m_pInput_tex->get_width() != m_output_tex.get_width()) || (m_pInput_tex->get_height() != m_output_tex.get_height()) || (m_pInput_tex->get_num_faces() != m_output_tex.get_num_faces())) {
|
|
console::warning("Unable to compute image statistics - input/output texture dimensions are different.");
|
|
} else {
|
|
uint num_faces = math::minimum(m_pInput_tex->get_num_faces(), m_output_tex.get_num_faces());
|
|
uint num_levels = math::minimum(m_pInput_tex->get_num_levels(), m_output_tex.get_num_levels());
|
|
|
|
if (!mip_stats)
|
|
num_levels = 1;
|
|
|
|
for (uint face = 0; face < num_faces; face++) {
|
|
for (uint level = 0; level < num_levels; level++) {
|
|
image_u8 a, b;
|
|
image_u8* pA = m_pInput_tex->get_level_image(face, level, a);
|
|
image_u8* pB = m_output_tex.get_level_image(face, level, b);
|
|
|
|
if (pA && pB) {
|
|
image_u8 grayscale_a, grayscale_b;
|
|
if (grayscale_sampling) {
|
|
grayscale_a = *pA;
|
|
grayscale_a.convert_to_grayscale();
|
|
pA = &grayscale_a;
|
|
|
|
grayscale_b = *pB;
|
|
grayscale_b.convert_to_grayscale();
|
|
pB = &grayscale_b;
|
|
}
|
|
|
|
console::info("Face %u Mipmap level %u statistics:", face, level);
|
|
image_utils::print_image_metrics(*pA, *pB);
|
|
|
|
if ((pA->has_rgb()) || (pB->has_rgb()))
|
|
image_utils::print_ssim(*pA, *pB);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pCSVStatsFile) {
|
|
// FIXME: This is kind of a hack, and should be combined with the code above.
|
|
image_u8 a, b;
|
|
image_u8* pA = m_pInput_tex->get_level_image(0, 0, a);
|
|
image_u8* pB = m_output_tex.get_level_image(0, 0, b);
|
|
if (pA && pB) {
|
|
image_u8 grayscale_a, grayscale_b;
|
|
if (grayscale_sampling) {
|
|
grayscale_a = *pA;
|
|
grayscale_a.convert_to_grayscale();
|
|
pA = &grayscale_a;
|
|
|
|
grayscale_b = *pB;
|
|
grayscale_b.convert_to_grayscale();
|
|
pB = &grayscale_b;
|
|
}
|
|
|
|
image_utils::error_metrics rgb_error;
|
|
image_utils::error_metrics luma_error;
|
|
if (rgb_error.compute(*pA, *pB, 0, 3, false) && luma_error.compute(*pA, *pB, 0, 0, true)) {
|
|
bool bCSVStatsFileExists = file_utils::does_file_exist(pCSVStatsFile);
|
|
FILE* pFile;
|
|
crn_fopen(&pFile, pCSVStatsFile, "a");
|
|
if (!pFile)
|
|
console::warning("Unable to append to CSV stats file: %s\n", pCSVStatsFile);
|
|
else {
|
|
if (!bCSVStatsFileExists)
|
|
fprintf(pFile, "name,width,height,miplevels,rgb_rms,luma_rms,effective_output_size,effective_bitrate\n");
|
|
dynamic_string filename;
|
|
file_utils::split_path(m_src_filename.get_ptr(), NULL, NULL, &filename, NULL);
|
|
|
|
uint64 effective_output_size = m_output_comp_file_size ? m_output_comp_file_size : m_output_file_size;
|
|
float bitrate = (effective_output_size * 8.0f) / m_total_output_pixels;
|
|
fprintf(pFile, "%s,%u,%u,%u,%f,%f,%u,%f\n",
|
|
filename.get_ptr(),
|
|
pB->get_width(), pB->get_height(), m_output_tex.get_num_levels(),
|
|
rgb_error.mRootMeanSquared, luma_error.mRootMeanSquared,
|
|
(uint32)effective_output_size, bitrate);
|
|
fclose(pFile);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void convert_stats::clear() {
|
|
m_src_filename.clear();
|
|
m_dst_filename.clear();
|
|
m_dst_file_type = texture_file_types::cFormatInvalid;
|
|
|
|
m_pInput_tex = NULL;
|
|
m_output_tex.clear();
|
|
|
|
m_input_file_size = 0;
|
|
m_total_input_pixels = 0;
|
|
|
|
m_output_file_size = 0;
|
|
m_total_output_pixels = 0;
|
|
|
|
m_output_comp_file_size = 0;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------
|
|
|
|
static crn_bool crn_progress_callback(crn_uint32 phase_index, crn_uint32 total_phases, crn_uint32 subphase_index, crn_uint32 total_subphases, void* pUser_data_ptr) {
|
|
progress_params& params = *static_cast<progress_params*>(pUser_data_ptr);
|
|
|
|
if (params.m_canceled)
|
|
return false;
|
|
if (!params.m_pParams->m_pProgress_func)
|
|
return true;
|
|
|
|
int percentage_complete = params.m_start_percentage + (int)(.5f + (phase_index + float(subphase_index) / total_subphases) * (100.0f - params.m_start_percentage) / total_phases);
|
|
|
|
percentage_complete = math::clamp<int>(percentage_complete, 0, 100);
|
|
|
|
if (!params.m_pParams->m_pProgress_func(percentage_complete, params.m_pParams->m_pProgress_user_data)) {
|
|
params.m_canceled = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool dxt_progress_callback_func(uint percentage_complete, void* pUser_data_ptr) {
|
|
progress_params& params = *static_cast<progress_params*>(pUser_data_ptr);
|
|
|
|
if (params.m_canceled)
|
|
return false;
|
|
|
|
if (!params.m_pParams->m_pProgress_func)
|
|
return true;
|
|
|
|
int scaled_percentage_complete = params.m_start_percentage + (percentage_complete * (100 - params.m_start_percentage)) / 100;
|
|
|
|
scaled_percentage_complete = math::clamp<int>(scaled_percentage_complete, 0, 100);
|
|
|
|
if (!params.m_pParams->m_pProgress_func(scaled_percentage_complete, params.m_pParams->m_pProgress_user_data)) {
|
|
params.m_canceled = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool convert_error(const convert_params& params, const char* pError_msg) {
|
|
params.m_status = false;
|
|
params.m_error_message = pError_msg;
|
|
|
|
remove(params.m_dst_filename.get_ptr());
|
|
|
|
return false;
|
|
}
|
|
|
|
static pixel_format choose_pixel_format(convert_params& params, const crn_comp_params& comp_params, const mipmapped_texture& src_tex, texture_type tex_type) {
|
|
const pixel_format src_fmt = src_tex.get_format();
|
|
const texture_file_types::format src_file_type = src_tex.get_source_file_type();
|
|
const bool is_normal_map = (tex_type == cTextureTypeNormalMap);
|
|
|
|
if (params.m_always_use_source_pixel_format)
|
|
return src_fmt;
|
|
|
|
// Attempt to choose a reasonable/sane output pixel format.
|
|
if (params.m_dst_file_type == texture_file_types::cFormatCRN) {
|
|
if (is_normal_map) {
|
|
if (pixel_format_helpers::is_dxt(src_fmt))
|
|
return src_fmt;
|
|
else
|
|
return PIXEL_FMT_DXT5_AGBR;
|
|
}
|
|
} else if (params.m_dst_file_type == texture_file_types::cFormatKTX) {
|
|
if ((src_file_type != texture_file_types::cFormatCRN) && (src_file_type != texture_file_types::cFormatKTX) && (src_file_type != texture_file_types::cFormatDDS)) {
|
|
if (is_normal_map) {
|
|
return pixel_format_helpers::has_alpha(src_fmt) ? PIXEL_FMT_A8R8G8B8 : PIXEL_FMT_R8G8B8;
|
|
} else if (pixel_format_helpers::is_grayscale(src_fmt)) {
|
|
if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return PIXEL_FMT_ETC2A;
|
|
else
|
|
return PIXEL_FMT_ETC1;
|
|
} else if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return PIXEL_FMT_ETC2A;
|
|
else
|
|
return PIXEL_FMT_ETC1;
|
|
}
|
|
} else if (params.m_dst_file_type == texture_file_types::cFormatDDS) {
|
|
if ((src_file_type != texture_file_types::cFormatCRN) && (src_file_type != texture_file_types::cFormatKTX) && (src_file_type != texture_file_types::cFormatDDS)) {
|
|
if (is_normal_map) {
|
|
return PIXEL_FMT_DXT5_AGBR;
|
|
} else if (pixel_format_helpers::is_grayscale(src_fmt)) {
|
|
if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return comp_params.get_flag(cCRNCompFlagDXT1AForTransparency) ? PIXEL_FMT_DXT1A : PIXEL_FMT_DXT5;
|
|
else
|
|
return PIXEL_FMT_DXT1;
|
|
} else if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return comp_params.get_flag(cCRNCompFlagDXT1AForTransparency) ? PIXEL_FMT_DXT1A : PIXEL_FMT_DXT5;
|
|
else
|
|
return PIXEL_FMT_DXT1;
|
|
}
|
|
} else {
|
|
// Destination is a regular image format.
|
|
if (pixel_format_helpers::is_grayscale(src_fmt)) {
|
|
if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return PIXEL_FMT_A8L8;
|
|
else
|
|
return PIXEL_FMT_L8;
|
|
} else if (pixel_format_helpers::has_alpha(src_fmt))
|
|
return PIXEL_FMT_A8R8G8B8;
|
|
else
|
|
return PIXEL_FMT_R8G8B8;
|
|
}
|
|
|
|
return src_fmt;
|
|
}
|
|
|
|
static void print_comp_params(const crn_comp_params& comp_params) {
|
|
console::debug("\nTexture conversion compression parameters:");
|
|
console::debug(" Desired bitrate: %3.3f", comp_params.m_target_bitrate);
|
|
console::debug(" CRN Quality: %i", comp_params.m_quality_level);
|
|
console::debug("CRN C endpoints/selectors: %u %u", comp_params.m_crn_color_endpoint_palette_size, comp_params.m_crn_color_selector_palette_size);
|
|
console::debug("CRN A endpoints/selectors: %u %u", comp_params.m_crn_alpha_endpoint_palette_size, comp_params.m_crn_alpha_selector_palette_size);
|
|
console::debug(" DXT both block types: %u, Alpha threshold: %u", comp_params.get_flag(cCRNCompFlagUseBothBlockTypes), comp_params.m_dxt1a_alpha_threshold);
|
|
console::debug(" DXT compression quality: %s", crn_get_dxt_quality_string(comp_params.m_dxt_quality));
|
|
console::debug(" Perceptual: %u, Large Blocks: %u", comp_params.get_flag(cCRNCompFlagPerceptual), comp_params.get_flag(cCRNCompFlagHierarchical));
|
|
console::debug(" Compressor: %s", get_dxt_compressor_name(comp_params.m_dxt_compressor_type));
|
|
console::debug(" Disable endpoint caching: %u", comp_params.get_flag(cCRNCompFlagDisableEndpointCaching));
|
|
console::debug(" Grayscale sampling: %u", comp_params.get_flag(cCRNCompFlagGrayscaleSampling));
|
|
console::debug(" Max helper threads: %u", comp_params.m_num_helper_threads);
|
|
console::debug("");
|
|
}
|
|
|
|
static void print_mipmap_params(const crn_mipmap_params& mipmap_params) {
|
|
console::debug("\nTexture conversion MIP-map parameters:");
|
|
console::debug(" Mode: %s", crn_get_mip_mode_name(mipmap_params.m_mode));
|
|
console::debug(" Filter: %s", crn_get_mip_filter_name(mipmap_params.m_filter));
|
|
console::debug("Gamma filtering: %u, Gamma: %2.2f", mipmap_params.m_gamma_filtering, mipmap_params.m_gamma);
|
|
console::debug(" Blurriness: %2.2f", mipmap_params.m_blurriness);
|
|
console::debug(" Renormalize: %u", mipmap_params.m_renormalize);
|
|
console::debug(" Tiled: %u", mipmap_params.m_tiled);
|
|
console::debug(" Max Levels: %u", mipmap_params.m_max_levels);
|
|
console::debug(" Min level size: %u", mipmap_params.m_min_mip_size);
|
|
console::debug(" window: %u %u %u %u", mipmap_params.m_window_left, mipmap_params.m_window_top, mipmap_params.m_window_right, mipmap_params.m_window_bottom);
|
|
console::debug(" scale mode: %s", crn_get_scale_mode_desc(mipmap_params.m_scale_mode));
|
|
console::debug(" scale: %f %f", mipmap_params.m_scale_x, mipmap_params.m_scale_y);
|
|
console::debug(" clamp: %u %u, clamp_scale: %u", mipmap_params.m_clamp_width, mipmap_params.m_clamp_height, mipmap_params.m_clamp_scale);
|
|
console::debug("");
|
|
}
|
|
|
|
void convert_params::print() {
|
|
console::debug("\nTexture conversion parameters:");
|
|
console::debug(" Resolution: %ux%u, Faces: %u, Levels: %u, Format: %s, X Flipped: %u, Y Flipped: %u",
|
|
m_pInput_texture->get_width(),
|
|
m_pInput_texture->get_height(),
|
|
m_pInput_texture->get_num_faces(),
|
|
m_pInput_texture->get_num_levels(),
|
|
pixel_format_helpers::get_pixel_format_string(m_pInput_texture->get_format()),
|
|
m_pInput_texture->is_x_flipped(),
|
|
m_pInput_texture->is_y_flipped());
|
|
|
|
console::debug(" texture_type: %s", get_texture_type_desc(m_texture_type));
|
|
console::debug(" dst_filename: %s", m_dst_filename.get_ptr());
|
|
console::debug(" dst_file_type: %s", texture_file_types::get_extension(m_dst_file_type));
|
|
console::debug(" dst_format: %s", pixel_format_helpers::get_pixel_format_string(m_dst_format));
|
|
console::debug(" quick: %u", m_quick);
|
|
console::debug(" use_source_format: %u", m_always_use_source_pixel_format);
|
|
console::debug(" Y Flip: %u", m_y_flip);
|
|
console::debug(" Unflip: %u", m_unflip);
|
|
}
|
|
|
|
static bool write_compressed_texture(
|
|
mipmapped_texture& work_tex, convert_params& params, crn_comp_params& comp_params, pixel_format dst_format, progress_params& progress_state, bool perceptual, convert_stats& stats) {
|
|
comp_params.m_file_type = (params.m_dst_file_type == texture_file_types::cFormatCRN) ? cCRNFileTypeCRN : cCRNFileTypeDDS;
|
|
|
|
comp_params.m_pProgress_func = crn_progress_callback;
|
|
comp_params.m_pProgress_func_data = &progress_state;
|
|
comp_params.set_flag(cCRNCompFlagPerceptual, perceptual);
|
|
|
|
crn_format crn_fmt = pixel_format_helpers::convert_pixel_format_to_best_crn_format(dst_format);
|
|
comp_params.m_format = crn_fmt;
|
|
|
|
console::message("Writing %s texture to file: \"%s\"", crn_get_format_string(crn_fmt), params.m_dst_filename.get_ptr());
|
|
|
|
uint32 actual_quality_level;
|
|
float actual_bitrate;
|
|
bool status = work_tex.write_to_file(params.m_dst_filename.get_ptr(), params.m_dst_file_type, &comp_params, &actual_quality_level, &actual_bitrate);
|
|
if (!status)
|
|
return convert_error(params, "Failed writing output file!");
|
|
|
|
if (!params.m_no_stats) {
|
|
if (!stats.init(params.m_pInput_texture->get_source_filename().get_ptr(), params.m_dst_filename.get_ptr(), *params.m_pIntermediate_texture, params.m_dst_file_type, params.m_lzma_stats)) {
|
|
console::warning("Unable to compute output statistics for file: %s", params.m_pInput_texture->get_source_filename().get_ptr());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool convert_and_write_normal_texture(mipmapped_texture& work_tex, convert_params& params, const crn_comp_params& comp_params, pixel_format dst_format, progress_params& progress_state, bool formats_differ, bool perceptual, convert_stats& stats) {
|
|
if (formats_differ) {
|
|
dxt_image::pack_params pack_params;
|
|
|
|
pack_params.m_perceptual = perceptual;
|
|
pack_params.m_compressor = comp_params.m_dxt_compressor_type;
|
|
pack_params.m_pProgress_callback = dxt_progress_callback_func;
|
|
pack_params.m_pProgress_callback_user_data_ptr = &progress_state;
|
|
pack_params.m_dxt1a_alpha_threshold = comp_params.m_dxt1a_alpha_threshold;
|
|
pack_params.m_quality = comp_params.m_dxt_quality;
|
|
pack_params.m_endpoint_caching = !comp_params.get_flag(cCRNCompFlagDisableEndpointCaching);
|
|
pack_params.m_grayscale_sampling = comp_params.get_flag(cCRNCompFlagGrayscaleSampling);
|
|
if ((!comp_params.get_flag(cCRNCompFlagUseBothBlockTypes)) && (!comp_params.get_flag(cCRNCompFlagDXT1AForTransparency)))
|
|
pack_params.m_use_both_block_types = false;
|
|
|
|
pack_params.m_num_helper_threads = comp_params.m_num_helper_threads;
|
|
pack_params.m_use_transparent_indices_for_black = comp_params.get_flag(cCRNCompFlagUseTransparentIndicesForBlack);
|
|
|
|
console::info("Converting texture format from %s to %s", pixel_format_helpers::get_pixel_format_string(work_tex.get_format()), pixel_format_helpers::get_pixel_format_string(dst_format));
|
|
|
|
timer tm;
|
|
tm.start();
|
|
|
|
bool status = work_tex.convert(dst_format, pack_params);
|
|
|
|
double t = tm.get_elapsed_secs();
|
|
|
|
console::info("");
|
|
|
|
if (!status) {
|
|
if (progress_state.m_canceled) {
|
|
params.m_canceled = true;
|
|
return false;
|
|
} else {
|
|
return convert_error(params, "Failed converting texture to output format!");
|
|
}
|
|
}
|
|
|
|
console::info("Texture format conversion took %3.3fs", t);
|
|
}
|
|
|
|
if (params.m_write_mipmaps_to_multiple_files) {
|
|
for (uint f = 0; f < work_tex.get_num_faces(); f++) {
|
|
for (uint l = 0; l < work_tex.get_num_levels(); l++) {
|
|
dynamic_string filename(params.m_dst_filename.get_ptr());
|
|
|
|
dynamic_string drv, dir, fn, ext;
|
|
if (!file_utils::split_path(params.m_dst_filename.get_ptr(), &drv, &dir, &fn, &ext))
|
|
return false;
|
|
|
|
fn += dynamic_string(cVarArg, "_face%u_mip%u", f, l).get_ptr();
|
|
filename = drv + dir + fn + ext;
|
|
|
|
mip_level* pLevel = work_tex.get_level(f, l);
|
|
|
|
face_vec face(1);
|
|
face[0].push_back(crnlib_new<mip_level>(*pLevel));
|
|
|
|
mipmapped_texture new_tex;
|
|
new_tex.assign(face);
|
|
|
|
console::info("Writing texture face %u mip level %u to file %s", f, l, filename.get_ptr());
|
|
|
|
if (!new_tex.write_to_file(filename.get_ptr(), params.m_dst_file_type, NULL, NULL, NULL))
|
|
return convert_error(params, "Failed writing output file!");
|
|
}
|
|
}
|
|
} else {
|
|
console::message("Writing texture to file: \"%s\"", params.m_dst_filename.get_ptr());
|
|
|
|
if (!work_tex.write_to_file(params.m_dst_filename.get_ptr(), params.m_dst_file_type, NULL, NULL, NULL))
|
|
return convert_error(params, "Failed writing output file!");
|
|
|
|
if (!params.m_no_stats) {
|
|
if (!stats.init(params.m_pInput_texture->get_source_filename().get_ptr(), params.m_dst_filename.get_ptr(), *params.m_pIntermediate_texture, params.m_dst_file_type, params.m_lzma_stats)) {
|
|
console::warning("Unable to compute output statistics for file: %s", params.m_pInput_texture->get_source_filename().get_ptr());
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool process(convert_params& params, convert_stats& stats) {
|
|
texture_type tex_type = params.m_texture_type;
|
|
|
|
crn_comp_params comp_params(params.m_comp_params);
|
|
crn_mipmap_params mipmap_params(params.m_mipmap_params);
|
|
|
|
progress_params progress_state;
|
|
progress_state.m_pParams = ¶ms;
|
|
progress_state.m_canceled = false;
|
|
progress_state.m_start_percentage = 0;
|
|
|
|
params.m_status = false;
|
|
params.m_error_message.clear();
|
|
|
|
if (params.m_pIntermediate_texture) {
|
|
crnlib_delete(params.m_pIntermediate_texture);
|
|
params.m_pIntermediate_texture = NULL;
|
|
}
|
|
|
|
params.m_pIntermediate_texture = crnlib_new<mipmapped_texture>(*params.m_pInput_texture);
|
|
|
|
mipmapped_texture& work_tex = *params.m_pInput_texture;
|
|
|
|
if ((params.m_unflip) && (work_tex.is_flipped())) {
|
|
console::info("Unflipping texture");
|
|
work_tex.unflip(true, true);
|
|
}
|
|
|
|
if (params.m_y_flip) {
|
|
console::info("Flipping texture on Y axis");
|
|
|
|
// This is awkward - if we're writing to KTX, then go ahead and properly update the work texture's orientation flags.
|
|
// Otherwise, don't bother updating the orientation flags because the writer may then attempt to unflip the texture before writing to formats
|
|
// that don't support flipped textures (ugh).
|
|
const bool bOutputFormatSupportsFlippedTextures = params.m_dst_file_type == texture_file_types::cFormatKTX;
|
|
if (!work_tex.flip_y(bOutputFormatSupportsFlippedTextures)) {
|
|
console::warning("Failed flipping texture on Y axis");
|
|
}
|
|
}
|
|
|
|
if ((params.m_dst_format != PIXEL_FMT_INVALID) && (pixel_format_helpers::is_alpha_only(params.m_dst_format))) {
|
|
if ((work_tex.get_comp_flags() & pixel_format_helpers::cCompFlagAValid) == 0) {
|
|
console::warning("Output format is alpha-only, but input doesn't have alpha, so setting alpha to luminance.");
|
|
|
|
work_tex.convert(PIXEL_FMT_A8, crnlib::dxt_image::pack_params());
|
|
|
|
if (tex_type == cTextureTypeNormalMap)
|
|
tex_type = cTextureTypeRegularMap;
|
|
}
|
|
}
|
|
|
|
pixel_format dst_format = params.m_dst_format;
|
|
if (pixel_format_helpers::is_dxt(dst_format)) {
|
|
if ((params.m_dst_file_type != texture_file_types::cFormatCRN) &&
|
|
(params.m_dst_file_type != texture_file_types::cFormatDDS) &&
|
|
(params.m_dst_file_type != texture_file_types::cFormatKTX)) {
|
|
console::warning("Output file format does not support DXTc - automatically choosing a non-DXT pixel format.");
|
|
dst_format = PIXEL_FMT_INVALID;
|
|
}
|
|
}
|
|
|
|
if (dst_format == PIXEL_FMT_INVALID) {
|
|
// Caller didn't specify a format to use, so try to pick something reasonable.
|
|
// This is actually much trickier than it seems, and the current approach kind of sucks.
|
|
dst_format = choose_pixel_format(params, comp_params, work_tex, tex_type);
|
|
}
|
|
|
|
if ((dst_format == PIXEL_FMT_DXT1) && (comp_params.get_flag(cCRNCompFlagDXT1AForTransparency)))
|
|
dst_format = PIXEL_FMT_DXT1A;
|
|
else if (dst_format == PIXEL_FMT_DXT1A)
|
|
comp_params.set_flag(cCRNCompFlagDXT1AForTransparency, true);
|
|
|
|
if ((dst_format == PIXEL_FMT_DXT1A) && (params.m_dst_file_type == texture_file_types::cFormatCRN)) {
|
|
console::warning("CRN file format does not support DXT1A compressed textures - converting to DXT5 instead.");
|
|
dst_format = PIXEL_FMT_DXT5;
|
|
}
|
|
|
|
const bool is_normal_map = (tex_type == cTextureTypeNormalMap);
|
|
bool perceptual = comp_params.get_flag(cCRNCompFlagPerceptual);
|
|
if (is_normal_map) {
|
|
perceptual = false;
|
|
mipmap_params.m_gamma_filtering = false;
|
|
}
|
|
|
|
if (pixel_format_helpers::is_pixel_format_non_srgb(dst_format)) {
|
|
if (perceptual) {
|
|
console::message("Output pixel format is swizzled or not RGB, disabling perceptual color metrics");
|
|
perceptual = false;
|
|
}
|
|
}
|
|
|
|
if (pixel_format_helpers::is_normal_map(dst_format)) {
|
|
if (perceptual)
|
|
console::message("Output pixel format is intended for normal maps, disabling perceptual color metrics");
|
|
|
|
perceptual = false;
|
|
}
|
|
|
|
bool generate_mipmaps = texture_file_types::supports_mipmaps(params.m_dst_file_type);
|
|
if ((params.m_write_mipmaps_to_multiple_files) &&
|
|
((params.m_dst_file_type != texture_file_types::cFormatCRN) && (params.m_dst_file_type != texture_file_types::cFormatDDS) && (params.m_dst_file_type != texture_file_types::cFormatKTX))) {
|
|
generate_mipmaps = true;
|
|
}
|
|
|
|
if (params.m_param_debugging) {
|
|
params.print();
|
|
|
|
print_comp_params(comp_params);
|
|
print_mipmap_params(mipmap_params);
|
|
}
|
|
|
|
if (!create_texture_mipmaps(work_tex, comp_params, mipmap_params, generate_mipmaps))
|
|
return convert_error(params, "Failed creating texture mipmaps!");
|
|
|
|
bool formats_differ = work_tex.get_format() != dst_format;
|
|
if (formats_differ) {
|
|
if (pixel_format_helpers::is_dxt1(work_tex.get_format()) && pixel_format_helpers::is_dxt1(dst_format))
|
|
formats_differ = false;
|
|
}
|
|
|
|
bool status = false;
|
|
|
|
timer t;
|
|
t.start();
|
|
|
|
if ((params.m_dst_file_type == texture_file_types::cFormatCRN) ||
|
|
((params.m_dst_file_type == texture_file_types::cFormatDDS) && (pixel_format_helpers::is_dxt(dst_format)) &&
|
|
//((formats_differ) || (comp_params.m_target_bitrate > 0.0f) || (comp_params.m_quality_level < cCRNMaxQualityLevel))
|
|
((comp_params.m_target_bitrate > 0.0f) || (comp_params.m_quality_level < cCRNMaxQualityLevel)))) {
|
|
status = write_compressed_texture(work_tex, params, comp_params, dst_format, progress_state, perceptual, stats);
|
|
} else {
|
|
if ((comp_params.m_target_bitrate > 0.0f) || (comp_params.m_quality_level < cCRNMaxQualityLevel)) {
|
|
console::warning("Target bitrate/quality level is not supported for this output file format.\n");
|
|
}
|
|
status = convert_and_write_normal_texture(work_tex, params, comp_params, dst_format, progress_state, formats_differ, perceptual, stats);
|
|
}
|
|
|
|
console::progress("");
|
|
|
|
if (progress_state.m_canceled) {
|
|
params.m_canceled = true;
|
|
return false;
|
|
}
|
|
|
|
double total_write_time = t.get_elapsed_secs();
|
|
|
|
if (status) {
|
|
if (params.m_param_debugging)
|
|
console::info("Work texture format: %s, desired destination format: %s", pixel_format_helpers::get_pixel_format_string(work_tex.get_format()), pixel_format_helpers::get_pixel_format_string(dst_format));
|
|
|
|
console::message("Texture successfully written in %3.3fs", total_write_time);
|
|
} else {
|
|
dynamic_string str;
|
|
|
|
if (work_tex.get_last_error().is_empty())
|
|
str.format("Failed writing texture to file \"%s\"", params.m_dst_filename.get_ptr());
|
|
else
|
|
str.format("Failed writing texture to file \"%s\", Reason: %s", params.m_dst_filename.get_ptr(), work_tex.get_last_error().get_ptr());
|
|
|
|
return convert_error(params, str.get_ptr());
|
|
}
|
|
|
|
if (params.m_debugging) {
|
|
crnlib_print_mem_stats();
|
|
}
|
|
|
|
params.m_status = true;
|
|
return true;
|
|
}
|
|
|
|
} // namespace texture_conversion
|
|
|
|
} // namespace crnlib
|