522 lines
18 KiB
C++
522 lines
18 KiB
C++
// File: crn_texture_comp.cpp
|
|
// See Copyright Notice and license at the end of inc/crnlib.h
|
|
#include "crn_core.h"
|
|
#include "crn_texture_comp.h"
|
|
#include "crn_dds_comp.h"
|
|
#include "crn_console.h"
|
|
#include "crn_rect.h"
|
|
|
|
namespace crnlib
|
|
{
|
|
static itexture_comp *create_texture_comp(crn_file_type file_type)
|
|
{
|
|
if (file_type == cCRNFileTypeCRN)
|
|
return crnlib_new<crn_comp>();
|
|
else if (file_type == cCRNFileTypeDDS)
|
|
return crnlib_new<dds_comp>();
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
bool create_compressed_texture(const crn_comp_params ¶ms, crnlib::vector<uint8> &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate)
|
|
{
|
|
crn_comp_params local_params(params);
|
|
|
|
if (pixel_format_helpers::is_crn_format_non_srgb(local_params.m_format))
|
|
{
|
|
if (local_params.get_flag(cCRNCompFlagPerceptual))
|
|
{
|
|
console::info("Output pixel format is swizzled or not RGB, disabling perceptual color metrics");
|
|
|
|
// Destination compressed pixel format is swizzled or not RGB at all, so be sure perceptual colorspace metrics are disabled.
|
|
local_params.set_flag(cCRNCompFlagPerceptual, false);
|
|
}
|
|
}
|
|
|
|
if (pActual_quality_level) *pActual_quality_level = 0;
|
|
if (pActual_bitrate) *pActual_bitrate = 0.0f;
|
|
|
|
comp_data.resize(0);
|
|
|
|
itexture_comp *pTexture_comp = create_texture_comp(local_params.m_file_type);
|
|
if (!pTexture_comp)
|
|
return false;
|
|
|
|
if (!pTexture_comp->compress_init(local_params))
|
|
{
|
|
crnlib_delete(pTexture_comp);
|
|
return false;
|
|
}
|
|
|
|
if ( (local_params.m_target_bitrate <= 0.0f) ||
|
|
(local_params.m_format == cCRNFmtDXT3) ||
|
|
((local_params.m_file_type == cCRNFileTypeCRN) && ((local_params.m_flags & cCRNCompFlagManualPaletteSizes) != 0))
|
|
)
|
|
{
|
|
if ( (local_params.m_file_type == cCRNFileTypeCRN) ||
|
|
((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel)) )
|
|
{
|
|
console::info("Compressing using quality level %i", local_params.m_quality_level);
|
|
}
|
|
if (local_params.m_format == cCRNFmtDXT3)
|
|
{
|
|
if (local_params.m_file_type == cCRNFileTypeCRN)
|
|
console::warning("CRN format doesn't support DXT3");
|
|
else if ((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel))
|
|
console::warning("Clustered DDS compressor doesn't support DXT3");
|
|
}
|
|
if (!pTexture_comp->compress_pass(local_params, pActual_bitrate))
|
|
{
|
|
crnlib_delete(pTexture_comp);
|
|
return false;
|
|
}
|
|
|
|
comp_data.swap(pTexture_comp->get_comp_data());
|
|
|
|
if ((pActual_quality_level) && (local_params.m_target_bitrate <= 0.0))
|
|
*pActual_quality_level = local_params.m_quality_level;
|
|
|
|
crnlib_delete(pTexture_comp);
|
|
return true;
|
|
}
|
|
|
|
// Interpolative search to find closest quality level to target bitrate.
|
|
const int cLowestQuality = 0;
|
|
const int cHighestQuality = cCRNMaxQualityLevel;
|
|
const int cNumQualityLevels = cHighestQuality - cLowestQuality + 1;
|
|
|
|
float best_bitrate = 1e+10f;
|
|
int best_quality_level = -1;
|
|
const uint cMaxIterations = 8;
|
|
|
|
for ( ; ; )
|
|
{
|
|
int low_quality = cLowestQuality;
|
|
int high_quality = cHighestQuality;
|
|
|
|
float cached_bitrates[cNumQualityLevels];
|
|
for (int i = 0; i < cNumQualityLevels; i++)
|
|
cached_bitrates[i] = -1.0f;
|
|
|
|
float highest_bitrate = 0.0f;
|
|
|
|
uint iter_count = 0;
|
|
bool force_binary_search = false;
|
|
|
|
while (low_quality <= high_quality)
|
|
{
|
|
if (params.m_flags & cCRNCompFlagDebugging)
|
|
{
|
|
console::debug("Quality level bracket: [%u, %u]", low_quality, high_quality);
|
|
}
|
|
|
|
int trial_quality = (low_quality + high_quality) / 2;
|
|
|
|
if ((iter_count) && (!force_binary_search))
|
|
{
|
|
int bracket_low = trial_quality;
|
|
while ((cached_bitrates[bracket_low] < 0) && (bracket_low > cLowestQuality))
|
|
bracket_low--;
|
|
|
|
if (cached_bitrates[bracket_low] < 0)
|
|
trial_quality = static_cast<int>(math::lerp<float>((float)low_quality, (float)high_quality, .33f));
|
|
else
|
|
{
|
|
int bracket_high = trial_quality + 1;
|
|
if (bracket_high <= cHighestQuality)
|
|
{
|
|
while ((cached_bitrates[bracket_high] < 0) && (bracket_high < cHighestQuality))
|
|
bracket_high++;
|
|
|
|
if (cached_bitrates[bracket_high] >= 0)
|
|
{
|
|
float bracket_low_bitrate = cached_bitrates[bracket_low];
|
|
float bracket_high_bitrate = cached_bitrates[bracket_high];
|
|
|
|
if ((bracket_low_bitrate < bracket_high_bitrate) &&
|
|
(bracket_low_bitrate < local_params.m_target_bitrate) &&
|
|
(bracket_high_bitrate >= local_params.m_target_bitrate))
|
|
{
|
|
int quality = low_quality + static_cast<int>( ((local_params.m_target_bitrate - bracket_low_bitrate) * (high_quality - low_quality)) / (bracket_high_bitrate - bracket_low_bitrate) );
|
|
|
|
if ((quality >= low_quality) && (quality <= high_quality))
|
|
{
|
|
trial_quality = quality;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console::info("Compressing to quality level %u", trial_quality);
|
|
|
|
float bitrate = 0.0f;
|
|
|
|
local_params.m_quality_level = trial_quality;
|
|
|
|
if (!pTexture_comp->compress_pass(local_params, &bitrate))
|
|
{
|
|
crnlib_delete(pTexture_comp);
|
|
return false;
|
|
}
|
|
|
|
cached_bitrates[trial_quality] = bitrate;
|
|
|
|
highest_bitrate = math::maximum(highest_bitrate, bitrate);
|
|
|
|
console::info("\nTried quality level %u, bpp: %3.3f", trial_quality, bitrate);
|
|
|
|
if ( (best_quality_level < 0) ||
|
|
((bitrate <= local_params.m_target_bitrate) && (best_bitrate > local_params.m_target_bitrate)) ||
|
|
(((bitrate <= local_params.m_target_bitrate) || (best_bitrate > local_params.m_target_bitrate)) && (fabs(bitrate - local_params.m_target_bitrate) < fabs(best_bitrate - local_params.m_target_bitrate)))
|
|
)
|
|
{
|
|
best_bitrate = bitrate;
|
|
comp_data.swap(pTexture_comp->get_comp_data());
|
|
best_quality_level = trial_quality;
|
|
if (params.m_flags & cCRNCompFlagDebugging)
|
|
{
|
|
console::debug("Choose new best quality level");
|
|
}
|
|
|
|
if ((best_bitrate <= local_params.m_target_bitrate) && (fabs(best_bitrate - local_params.m_target_bitrate) < .005f))
|
|
break;
|
|
}
|
|
|
|
if (bitrate > local_params.m_target_bitrate)
|
|
high_quality = trial_quality - 1;
|
|
else
|
|
low_quality = trial_quality + 1;
|
|
|
|
iter_count++;
|
|
if (iter_count > cMaxIterations)
|
|
{
|
|
force_binary_search = true;
|
|
}
|
|
}
|
|
|
|
if (((local_params.m_flags & cCRNCompFlagHierarchical) != 0) &&
|
|
(highest_bitrate < local_params.m_target_bitrate) &&
|
|
(fabs(best_bitrate - local_params.m_target_bitrate) >= .005f))
|
|
{
|
|
console::info("Unable to achieve desired bitrate - disabling adaptive block sizes and retrying search.");
|
|
|
|
local_params.m_flags &= ~cCRNCompFlagHierarchical;
|
|
|
|
crnlib_delete(pTexture_comp);
|
|
pTexture_comp = create_texture_comp(local_params.m_file_type);
|
|
|
|
if (!pTexture_comp->compress_init(local_params))
|
|
{
|
|
crnlib_delete(pTexture_comp);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
crnlib_delete(pTexture_comp);
|
|
pTexture_comp = NULL;
|
|
|
|
if (best_quality_level < 0)
|
|
return false;
|
|
|
|
if (pActual_quality_level) *pActual_quality_level = best_quality_level;
|
|
if (pActual_bitrate) *pActual_bitrate = best_bitrate;
|
|
|
|
console::printf("Selected quality level %u bpp: %f", best_quality_level, best_bitrate);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool create_dds_tex(const crn_comp_params ¶ms, mipmapped_texture &dds_tex)
|
|
{
|
|
image_u8 images[cCRNMaxFaces][cCRNMaxLevels];
|
|
|
|
bool has_alpha = false;
|
|
for (uint face_index = 0; face_index < params.m_faces; face_index++)
|
|
{
|
|
for (uint level_index = 0; level_index < params.m_levels; level_index++)
|
|
{
|
|
const uint width = math::maximum(1U, params.m_width >> level_index);
|
|
const uint height = math::maximum(1U, params.m_height >> level_index);
|
|
|
|
if (!params.m_pImages[face_index][level_index])
|
|
return false;
|
|
|
|
images[face_index][level_index].alias((color_quad_u8*)params.m_pImages[face_index][level_index], width, height);
|
|
if (!has_alpha)
|
|
has_alpha = image_utils::has_alpha(images[face_index][level_index]);
|
|
}
|
|
}
|
|
|
|
for (uint face_index = 0; face_index < params.m_faces; face_index++)
|
|
for (uint level_index = 0; level_index < params.m_levels; level_index++)
|
|
images[face_index][level_index].set_component_valid(3, has_alpha);
|
|
|
|
face_vec faces(params.m_faces);
|
|
|
|
for (uint face_index = 0; face_index < params.m_faces; face_index++)
|
|
{
|
|
for (uint level_index = 0; level_index < params.m_levels; level_index++)
|
|
{
|
|
mip_level *pMip = crnlib_new<mip_level>();
|
|
|
|
image_u8 *pImage = crnlib_new<image_u8>();
|
|
pImage->swap(images[face_index][level_index]);
|
|
pMip->assign(pImage);
|
|
|
|
faces[face_index].push_back(pMip);
|
|
}
|
|
}
|
|
|
|
dds_tex.assign(faces);
|
|
|
|
#ifdef CRNLIB_BUILD_DEBUG
|
|
CRNLIB_ASSERT(dds_tex.check());
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
bool create_texture_mipmaps(mipmapped_texture &work_tex, const crn_comp_params ¶ms, const crn_mipmap_params &mipmap_params, bool generate_mipmaps)
|
|
{
|
|
crn_comp_params new_params(params);
|
|
|
|
bool generate_new_mips = false;
|
|
|
|
switch (mipmap_params.m_mode)
|
|
{
|
|
case cCRNMipModeUseSourceOrGenerateMips:
|
|
{
|
|
if (work_tex.get_num_levels() == 1)
|
|
generate_new_mips = true;
|
|
break;
|
|
}
|
|
case cCRNMipModeUseSourceMips:
|
|
{
|
|
break;
|
|
}
|
|
case cCRNMipModeGenerateMips:
|
|
{
|
|
generate_new_mips = true;
|
|
break;
|
|
}
|
|
case cCRNMipModeNoMips:
|
|
{
|
|
work_tex.discard_mipmaps();
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
CRNLIB_ASSERT(0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
rect window_rect(mipmap_params.m_window_left, mipmap_params.m_window_top, mipmap_params.m_window_right, mipmap_params.m_window_bottom);
|
|
|
|
if (!window_rect.is_empty())
|
|
{
|
|
if (work_tex.get_num_faces() > 1)
|
|
{
|
|
console::warning("Can't crop cubemap textures");
|
|
}
|
|
else
|
|
{
|
|
console::info("Cropping input texture from window (%ux%u)-(%ux%u)", window_rect.get_left(), window_rect.get_top(), window_rect.get_right(), window_rect.get_bottom());
|
|
|
|
if (!work_tex.crop(window_rect.get_left(), window_rect.get_top(), window_rect.get_width(), window_rect.get_height()))
|
|
console::warning("Failed cropping window rect");
|
|
}
|
|
}
|
|
|
|
int new_width = work_tex.get_width();
|
|
int new_height = work_tex.get_height();
|
|
|
|
if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
|
|
{
|
|
if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
|
|
{
|
|
if (!mipmap_params.m_clamp_scale)
|
|
{
|
|
if (work_tex.get_num_faces() > 1)
|
|
{
|
|
console::warning("Can't crop cubemap textures");
|
|
}
|
|
else
|
|
{
|
|
new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
|
|
new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
|
|
console::info("Clamping input texture to %ux%u", new_width, new_height);
|
|
work_tex.crop(0, 0, new_width, new_height);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mipmap_params.m_scale_mode != cCRNSMDisabled)
|
|
{
|
|
bool is_pow2 = math::is_power_of_2((uint32)new_width) && math::is_power_of_2((uint32)new_height);
|
|
|
|
switch (mipmap_params.m_scale_mode)
|
|
{
|
|
case cCRNSMAbsolute:
|
|
{
|
|
new_width = (uint)mipmap_params.m_scale_x;
|
|
new_height = (uint)mipmap_params.m_scale_y;
|
|
break;
|
|
}
|
|
case cCRNSMRelative:
|
|
{
|
|
new_width = (uint)(mipmap_params.m_scale_x * new_width + .5f);
|
|
new_height = (uint)(mipmap_params.m_scale_y * new_height + .5f);
|
|
break;
|
|
}
|
|
case cCRNSMLowerPow2:
|
|
{
|
|
if (!is_pow2)
|
|
math::compute_lower_pow2_dim(new_width, new_height);
|
|
break;
|
|
}
|
|
case cCRNSMNearestPow2:
|
|
{
|
|
if (!is_pow2)
|
|
{
|
|
int lwidth = new_width;
|
|
int lheight = new_height;
|
|
math::compute_lower_pow2_dim(lwidth, lheight);
|
|
|
|
int uwidth = new_width;
|
|
int uheight = new_height;
|
|
math::compute_upper_pow2_dim(uwidth, uheight);
|
|
|
|
if (labs(new_width - lwidth) < labs(new_width - uwidth))
|
|
new_width = lwidth;
|
|
else
|
|
new_width = uwidth;
|
|
|
|
if (labs(new_height - lheight) < labs(new_height - uheight))
|
|
new_height = lheight;
|
|
else
|
|
new_height = uheight;
|
|
}
|
|
break;
|
|
}
|
|
case cCRNSMNextPow2:
|
|
{
|
|
if (!is_pow2)
|
|
math::compute_upper_pow2_dim(new_width, new_height);
|
|
break;
|
|
}
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
|
|
{
|
|
if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
|
|
{
|
|
if (mipmap_params.m_clamp_scale)
|
|
{
|
|
new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
|
|
new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
|
|
}
|
|
}
|
|
}
|
|
|
|
new_width = math::clamp<int>(new_width, 1, cCRNMaxLevelResolution);
|
|
new_height = math::clamp<int>(new_height, 1, cCRNMaxLevelResolution);
|
|
|
|
if ((new_width != (int)work_tex.get_width()) || (new_height != (int)work_tex.get_height()))
|
|
{
|
|
console::info("Resampling input texture to %ux%u", new_width, new_height);
|
|
|
|
const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);
|
|
|
|
bool srgb = mipmap_params.m_gamma_filtering != 0;
|
|
|
|
mipmapped_texture::resample_params res_params;
|
|
res_params.m_pFilter = pFilter;
|
|
res_params.m_wrapping = mipmap_params.m_tiled != 0;
|
|
if (work_tex.get_num_faces())
|
|
res_params.m_wrapping = false;
|
|
res_params.m_renormalize = mipmap_params.m_renormalize != 0;
|
|
res_params.m_filter_scale = 1.0f;
|
|
res_params.m_gamma = mipmap_params.m_gamma;
|
|
res_params.m_srgb = srgb;
|
|
res_params.m_multithreaded = (params.m_num_helper_threads > 0);
|
|
|
|
if (!work_tex.resize(new_width, new_height, res_params))
|
|
{
|
|
console::error("Failed resizing texture!");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((generate_new_mips) && (generate_mipmaps))
|
|
{
|
|
bool srgb = mipmap_params.m_gamma_filtering != 0;
|
|
|
|
const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);
|
|
|
|
mipmapped_texture::generate_mipmap_params gen_params;
|
|
gen_params.m_pFilter = pFilter;
|
|
gen_params.m_wrapping = mipmap_params.m_tiled != 0;
|
|
gen_params.m_renormalize = mipmap_params.m_renormalize != 0;
|
|
gen_params.m_filter_scale = mipmap_params.m_blurriness;
|
|
gen_params.m_gamma = mipmap_params.m_gamma;
|
|
gen_params.m_srgb = srgb;
|
|
gen_params.m_multithreaded = params.m_num_helper_threads > 0;
|
|
gen_params.m_max_mips = mipmap_params.m_max_levels;
|
|
gen_params.m_min_mip_size = mipmap_params.m_min_mip_size;
|
|
|
|
console::info("Generating mipmaps using filter \"%s\"", pFilter);
|
|
|
|
timer tm;
|
|
tm.start();
|
|
if (!work_tex.generate_mipmaps(gen_params, true))
|
|
{
|
|
console::error("Failed generating mipmaps!");
|
|
return false;
|
|
}
|
|
double t = tm.get_elapsed_secs();
|
|
|
|
console::info("Generated %u mipmap levels in %3.3fs", work_tex.get_num_levels() - 1, t);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool create_compressed_texture(const crn_comp_params ¶ms, const crn_mipmap_params &mipmap_params, crnlib::vector<uint8> &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate)
|
|
{
|
|
comp_data.resize(0);
|
|
if (pActual_bitrate) *pActual_bitrate = 0.0f;
|
|
if (pActual_quality_level) *pActual_quality_level = 0;
|
|
|
|
mipmapped_texture work_tex;
|
|
if (!create_dds_tex(params, work_tex))
|
|
{
|
|
console::error("Failed creating DDS texture from crn_comp_params!");
|
|
return false;
|
|
}
|
|
|
|
if (!create_texture_mipmaps(work_tex, params, mipmap_params, true))
|
|
return false;
|
|
|
|
crn_comp_params new_params(params);
|
|
new_params.m_levels = work_tex.get_num_levels();
|
|
memset(new_params.m_pImages, 0, sizeof(new_params.m_pImages));
|
|
|
|
for (uint f = 0; f < work_tex.get_num_faces(); f++)
|
|
for (uint l = 0; l < work_tex.get_num_levels(); l++)
|
|
new_params.m_pImages[f][l] = (uint32*)work_tex.get_level(f, l)->get_image()->get_ptr();
|
|
|
|
return create_compressed_texture(new_params, comp_data, pActual_quality_level, pActual_bitrate);
|
|
}
|
|
|
|
} // namespace crnlib
|
|
|