Introduction
This page shows how to dynamically link in crunch/crnlib v1.04 - Advanced DXTn texture compression library. This is useful for developers that ship content using the DXT1/5/N or 3DC compressed color/normal map/cubemap mipmapped texture formats.
The original crunch source can be found on github : https://github.com/BinomialLLC/crunch
A superb article explaining how it compresses can be found at : https://unity.com/blog/engine-platform/crunch-compression-of-etc-textures
Authors Comments
I personally use crunch to compress textures which are far smaller than their DXT equivalents without losing much fidelity/quality of their original image. This is obviously a subjective topic determining if the quality of the compression is sufficient for the requirement of the game. If not then dont use crunch !
It also helps reduce server space on remote servers and improves the download rates because of their reduced sizes.
Example crunch results on common albedo and normal textures
Grainy Wood, 1024x1024, frmt: dxt1, srgb:true, src: 4 mb, dds: 682 kb crn: 181 kb, mips:11, comp ratio: 22.63
Food Grape, 1024x1024. frmt: bc5, srgb:false, src: 2.06 mb, dds: 1365 kb, crn: 414 kb, mips:11, comp ratio: 5.09
Building crnlib.dll
To build the crnlib 64 bit x64 dll you will need Visual Studio Community 2022 (64-bit) Edition. I know it works on previous versions but this was the latest i used.
If you dont want to get the source code and build it then i have a tested x64 Windows DLL that you can simply use here.
crnlib.dll (1.3 MB)
Get the source code from GitHub - BinomialLLC/crunch: Advanced DXTc texture compression and transcoding library
Code Modifications
crnlib.cpp
Replace with
// File: crnlib.cpp
// See Copyright Notice and license at the end of inc/crnlib.h
#include "crn_core.h"
#include "../inc/crnlib.h"
#include "crn_comp.h"
#include "crn_dds_comp.h"
#include "crn_dynamic_stream.h"
#include "crn_buffer_stream.h"
#include "crn_ryg_dxt.hpp"
#include "crn_etc.h"
#include "../inc/crn_defs.h"
#include "crn_rg_etc1.h"
namespace crnlib {
static void* realloc_func(void* p, size_t size, size_t* pActual_size, bool movable, void*) {
return crnlib_realloc(p, size, pActual_size, movable);
}
static size_t msize_func(void* p, void*) {
return crnlib_msize(p);
}
class crnlib_global_initializer {
public:
crnlib_global_initializer() {
crn_threading_init();
crnlib_enable_fail_exceptions(true);
// Redirect crn_decomp.h's memory allocations into crnlib, which may be further redirected by the outside caller.
crnd::crnd_set_memory_callbacks(realloc_func, msize_func, NULL);
ryg_dxt::sInitDXT();
pack_etc1_block_init();
rg_etc1::pack_etc1_block_init();
}
};
crnlib_global_initializer g_crnlib_initializer;
} // namespace crnlib
using namespace crnlib;
const char* crn_get_format_string(crn_format fmt) {
return pixel_format_helpers::get_crn_format_string(fmt);
}
crn_uint32 crn_get_format_fourcc(crn_format fmt) {
return crnd::crnd_crn_format_to_fourcc(fmt);
}
crn_uint32 crn_get_format_bits_per_texel(crn_format fmt) {
return crnd::crnd_get_crn_format_bits_per_texel(fmt);
}
crn_uint32 crn_get_bytes_per_dxt_block(crn_format fmt) {
return crnd::crnd_get_bytes_per_dxt_block(fmt);
}
crn_format crn_get_fundamental_dxt_format(crn_format fmt) {
return crnd::crnd_get_fundamental_dxt_format(fmt);
}
const char* crn_get_file_type_ext(crn_file_type file_type) {
switch (file_type) {
case cCRNFileTypeDDS:
return "dds";
case cCRNFileTypeCRN:
return "crn";
default:
break;
}
return "?";
}
const char* crn_get_mip_mode_desc(crn_mip_mode m) {
switch (m) {
case cCRNMipModeUseSourceOrGenerateMips:
return "Use source/generate if none";
case cCRNMipModeUseSourceMips:
return "Only use source MIP maps (if any)";
case cCRNMipModeGenerateMips:
return "Always generate new MIP maps";
case cCRNMipModeNoMips:
return "No MIP maps";
default:
break;
}
return "?";
}
const char* crn_get_mip_mode_name(crn_mip_mode m) {
switch (m) {
case cCRNMipModeUseSourceOrGenerateMips:
return "UseSourceOrGenerate";
case cCRNMipModeUseSourceMips:
return "UseSource";
case cCRNMipModeGenerateMips:
return "Generate";
case cCRNMipModeNoMips:
return "None";
default:
break;
}
return "?";
}
const char* crn_get_mip_filter_name(crn_mip_filter f) {
switch (f) {
case cCRNMipFilterBox:
return "box";
case cCRNMipFilterTent:
return "tent";
case cCRNMipFilterLanczos4:
return "lanczos4";
case cCRNMipFilterMitchell:
return "mitchell";
case cCRNMipFilterKaiser:
return "kaiser";
default:
break;
}
return "?";
}
const char* crn_get_scale_mode_desc(crn_scale_mode sm) {
switch (sm) {
case cCRNSMDisabled:
return "disabled";
case cCRNSMAbsolute:
return "absolute";
case cCRNSMRelative:
return "relative";
case cCRNSMLowerPow2:
return "lowerpow2";
case cCRNSMNearestPow2:
return "nearestpow2";
case cCRNSMNextPow2:
return "nextpow2";
default:
break;
}
return "?";
}
const char* crn_get_dxt_quality_string(crn_dxt_quality q) {
switch (q) {
case cCRNDXTQualitySuperFast:
return "SuperFast";
case cCRNDXTQualityFast:
return "Fast";
case cCRNDXTQualityNormal:
return "Normal";
case cCRNDXTQualityBetter:
return "Better";
case cCRNDXTQualityUber:
return "Uber";
default:
break;
}
CRNLIB_ASSERT(false);
return "?";
}
extern "C"
{
void crn_free_block(void* pBlock)
{
crnlib_free(pBlock);
}
void* crn_compress(crn_uint32* imagedata,
crn_uint32 width,
crn_uint32 height,
crn_uint32 format,
crn_uint32 flags,
crn_uint32 quality_level,
crn_uint32 worker_threads,
crn_bool create_mipmaps,
crn_uint32& compressed_size,
crn_uint32* pActual_quality_level,
float* pActual_bitrate)
{
compressed_size = 0;
if (pActual_quality_level)
*pActual_quality_level = 0;
if (pActual_bitrate)
*pActual_bitrate = 0.0f;
crn_comp_params params;
params.clear();
params.m_width = width;
params.m_height = height;
params.m_format = (crn_format)format;
params.m_flags = flags;
params.m_quality_level = quality_level;
params.m_pImages[0][0] = (const crn_uint32*)imagedata;
params.m_num_helper_threads = worker_threads;
// Get access to the image
// crn_uint32* image = (crn_uint32*)params.m_pImages[0][0];
//*pActual_quality_level = image[5];
if (!params.check())
return NULL;
// Fill in mipmap parameters struct.
crn_mipmap_params mip_params;
mip_params.clear();
mip_params.m_mode = create_mipmaps ? cCRNMipModeGenerateMips : cCRNMipModeNoMips;
crnlib::vector<uint8> crn_file_data;
if (!create_compressed_texture(params, mip_params, crn_file_data, pActual_quality_level, pActual_bitrate))
return NULL;
compressed_size = crn_file_data.size();
return crn_file_data.assume_ownership();
}
void* crn_decompress(const void* pCRN_file_data, crn_uint32& file_size)
{
mipmapped_texture tex;
if (!tex.read_crn_from_memory(pCRN_file_data, file_size, "from_memory.crn")) {
file_size = 0;
return NULL;
}
file_size = 0;
dynamic_stream dds_file_data;
dds_file_data.reserve(128 * 1024);
data_stream_serializer serializer(dds_file_data);
if (!tex.write_dds(serializer))
return NULL;
dds_file_data.reserve(0);
file_size = static_cast<crn_uint32>(dds_file_data.get_size());
return dds_file_data.get_buf().assume_ownership();
}
bool crn_decompress_dds_to_images(
const void* pDDS_file_data,
crn_uint32 dds_file_size,
crn_uint32** ppImages,
crn_uint32& faces,
crn_uint32& levels,
crn_uint32& width,
crn_uint32& height)
{
mipmapped_texture tex;
buffer_stream in_stream(pDDS_file_data, dds_file_size);
data_stream_serializer in_serializer(in_stream);
if (!tex.read_dds(in_serializer))
return false;
if (tex.is_packed()) {
// TODO: Allow the user to disable uncooking of swizzled DXT5 formats?
bool uncook = true;
if (!tex.unpack_from_dxt(uncook))
return false;
}
faces = tex.get_num_faces();
levels = tex.get_num_levels();
width = tex.get_width();
height = tex.get_height();
for (uint32 f = 0; f < tex.get_num_faces(); f++) {
for (uint32 l = 0; l < tex.get_num_levels(); l++) {
mip_level* pLevel = tex.get_level(f, l);
image_u8* pImg = pLevel->get_image();
ppImages[l + tex.get_num_levels() * f] = static_cast<crn_uint32*>(pImg->get_pixel_buf().assume_ownership());
}
}
return true;
}
void crn_free_all_images(crn_uint32** ppImages, crn_uint32 faces, crn_uint32 levels)
{
for (uint32 f = 0; f < faces; f++)
for (uint32 l = 0; l < levels; l++)
crn_free_block(ppImages[l + levels * f]);
}
}
void* crn_compress(const crn_comp_params& comp_params, crn_uint32& compressed_size, crn_uint32* pActual_quality_level, float* pActual_bitrate)
{
compressed_size = 0;
if (pActual_quality_level)
*pActual_quality_level = 0;
if (pActual_bitrate)
*pActual_bitrate = 0.0f;
if (!comp_params.check())
return NULL;
crnlib::vector<uint8> crn_file_data;
if (!create_compressed_texture(comp_params, crn_file_data, pActual_quality_level, pActual_bitrate))
return NULL;
compressed_size = crn_file_data.size();
return crn_file_data.assume_ownership();
}
void* crn_compress(const crn_comp_params& comp_params, const crn_mipmap_params& mip_params, crn_uint32& compressed_size, crn_uint32* pActual_quality_level, float* pActual_bitrate) {
compressed_size = 0;
if (pActual_quality_level)
*pActual_quality_level = 0;
if (pActual_bitrate)
*pActual_bitrate = 0.0f;
if ((!comp_params.check()) || (!mip_params.check()))
return NULL;
crnlib::vector<uint8> crn_file_data;
if (!create_compressed_texture(comp_params, mip_params, crn_file_data, pActual_quality_level, pActual_bitrate))
return NULL;
compressed_size = crn_file_data.size();
return crn_file_data.assume_ownership();
}
void* crn_decompress_crn_to_dds(const void* pCRN_file_data, crn_uint32& file_size) {
mipmapped_texture tex;
if (!tex.read_crn_from_memory(pCRN_file_data, file_size, "from_memory.crn")) {
file_size = 0;
return NULL;
}
file_size = 0;
dynamic_stream dds_file_data;
dds_file_data.reserve(128 * 1024);
data_stream_serializer serializer(dds_file_data);
if (!tex.write_dds(serializer))
return NULL;
dds_file_data.reserve(0);
file_size = static_cast<crn_uint32>(dds_file_data.get_size());
return dds_file_data.get_buf().assume_ownership();
}
bool crn_decompress_dds_to_images(const void* pDDS_file_data, crn_uint32 dds_file_size, crn_uint32** ppImages, crn_texture_desc& tex_desc) {
memset(&tex_desc, 0, sizeof(tex_desc));
mipmapped_texture tex;
buffer_stream in_stream(pDDS_file_data, dds_file_size);
data_stream_serializer in_serializer(in_stream);
if (!tex.read_dds(in_serializer))
return false;
if (tex.is_packed()) {
// TODO: Allow the user to disable uncooking of swizzled DXT5 formats?
bool uncook = true;
if (!tex.unpack_from_dxt(uncook))
return false;
}
tex_desc.m_faces = tex.get_num_faces();
tex_desc.m_width = tex.get_width();
tex_desc.m_height = tex.get_height();
tex_desc.m_levels = tex.get_num_levels();
tex_desc.m_fmt_fourcc = (crn_uint32)tex.get_format();
for (uint32 f = 0; f < tex.get_num_faces(); f++) {
for (uint32 l = 0; l < tex.get_num_levels(); l++) {
mip_level* pLevel = tex.get_level(f, l);
image_u8* pImg = pLevel->get_image();
ppImages[l + tex.get_num_levels() * f] = static_cast<crn_uint32*>(pImg->get_pixel_buf().assume_ownership());
}
}
return true;
}
void crn_free_all_images(crn_uint32** ppImages, const crn_texture_desc& desc) {
for (uint32 f = 0; f < desc.m_faces; f++)
for (uint32 l = 0; l < desc.m_levels; l++)
crn_free_block(ppImages[l + desc.m_levels * f]);
}
// Simple low-level DXTn 4x4 block compressor API.
// Basically just a basic wrapper over the crnlib::dxt_image class.
namespace crnlib {
class crn_block_compressor {
CRNLIB_NO_COPY_OR_ASSIGNMENT_OP(crn_block_compressor);
public:
crn_block_compressor() {
}
bool init(const crn_comp_params& params) {
m_comp_params = params;
m_pack_params.init(params);
crn_format basic_crn_fmt = crnd::crnd_get_fundamental_dxt_format(params.m_format);
pixel_format basic_pixel_fmt = pixel_format_helpers::convert_crn_format_to_pixel_format(basic_crn_fmt);
if ((params.get_flag(cCRNCompFlagDXT1AForTransparency)) && (basic_pixel_fmt == PIXEL_FMT_DXT1))
basic_pixel_fmt = PIXEL_FMT_DXT1A;
if (!m_image.init(pixel_format_helpers::get_dxt_format(basic_pixel_fmt), cDXTBlockSize, cDXTBlockSize, false))
return false;
return true;
}
void compress_block(const crn_uint32* pPixels, void* pDst_block) {
if (m_image.is_valid()) {
m_image.set_block_pixels(0, 0, reinterpret_cast<const color_quad_u8*>(pPixels), m_pack_params, m_set_block_pixels_context);
memcpy(pDst_block, &m_image.get_element(0, 0, 0), m_image.get_bytes_per_block());
}
}
private:
dxt_image m_image;
crn_comp_params m_comp_params;
dxt_image::pack_params m_pack_params;
dxt_image::set_block_pixels_context m_set_block_pixels_context;
};
}
crn_block_compressor_context_t crn_create_block_compressor(const crn_comp_params& params) {
crn_block_compressor* pComp = crnlib_new<crn_block_compressor>();
if (!pComp->init(params)) {
crnlib_delete(pComp);
return NULL;
}
return pComp;
}
void crn_compress_block(crn_block_compressor_context_t pContext, const crn_uint32* pPixels, void* pDst_block) {
crn_block_compressor* pComp = static_cast<crn_block_compressor*>(pContext);
pComp->compress_block(pPixels, pDst_block);
}
void crn_free_block_compressor(crn_block_compressor_context_t pContext) {
crnlib_delete(static_cast<crn_block_compressor*>(pContext));
}
bool crn_decompress_block(const void* pSrc_block, crn_uint32* pDst_pixels_u32, crn_format crn_fmt) {
color_quad_u8* pDst_pixels = reinterpret_cast<color_quad_u8*>(pDst_pixels_u32);
switch (crn_get_fundamental_dxt_format(crn_fmt)) {
case cCRNFmtETC1: {
const etc1_block& block = *reinterpret_cast<const etc1_block*>(pSrc_block);
unpack_etc1(block, pDst_pixels, false);
break;
}
case cCRNFmtDXT1: {
const dxt1_block* pDXT1_block = reinterpret_cast<const dxt1_block*>(pSrc_block);
color_quad_u8 colors[cDXT1SelectorValues];
pDXT1_block->get_block_colors(colors, static_cast<uint16>(pDXT1_block->get_low_color()), static_cast<uint16>(pDXT1_block->get_high_color()));
for (uint i = 0; i < cDXTBlockSize * cDXTBlockSize; i++) {
const uint s = pDXT1_block->get_selector(i & 3, i >> 2);
pDst_pixels[i] = colors[s];
}
break;
}
case cCRNFmtDXT3: {
const dxt3_block* pDXT3_block = reinterpret_cast<const dxt3_block*>(pSrc_block);
const dxt1_block* pDXT1_block = reinterpret_cast<const dxt1_block*>(pSrc_block) + 1;
color_quad_u8 colors[cDXT1SelectorValues];
pDXT1_block->get_block_colors(colors, static_cast<uint16>(pDXT1_block->get_low_color()), static_cast<uint16>(pDXT1_block->get_high_color()));
for (uint i = 0; i < cDXTBlockSize * cDXTBlockSize; i++) {
const uint s = pDXT1_block->get_selector(i & 3, i >> 2);
const uint a = pDXT3_block->get_alpha(i & 3, i >> 2, true);
pDst_pixels[i] = colors[s];
pDst_pixels[i].a = static_cast<uint8>(a);
}
break;
}
case cCRNFmtDXT5: {
const dxt5_block* pDXT5_block = reinterpret_cast<const dxt5_block*>(pSrc_block);
const dxt1_block* pDXT1_block = reinterpret_cast<const dxt1_block*>(pSrc_block) + 1;
color_quad_u8 colors[cDXT1SelectorValues];
pDXT1_block->get_block_colors(colors, static_cast<uint16>(pDXT1_block->get_low_color()), static_cast<uint16>(pDXT1_block->get_high_color()));
uint values[cDXT5SelectorValues];
dxt5_block::get_block_values(values, pDXT5_block->get_low_alpha(), pDXT5_block->get_high_alpha());
for (uint i = 0; i < cDXTBlockSize * cDXTBlockSize; i++) {
const uint s = pDXT1_block->get_selector(i & 3, i >> 2);
const uint a = pDXT5_block->get_selector(i & 3, i >> 2);
pDst_pixels[i] = colors[s];
pDst_pixels[i].a = static_cast<uint8>(values[a]);
}
}
case cCRNFmtDXN_XY:
case cCRNFmtDXN_YX: {
const dxt5_block* pDXT5_block0 = reinterpret_cast<const dxt5_block*>(pSrc_block);
const dxt5_block* pDXT5_block1 = reinterpret_cast<const dxt5_block*>(pSrc_block) + 1;
uint values0[cDXT5SelectorValues];
dxt5_block::get_block_values(values0, pDXT5_block0->get_low_alpha(), pDXT5_block0->get_high_alpha());
uint values1[cDXT5SelectorValues];
dxt5_block::get_block_values(values1, pDXT5_block1->get_low_alpha(), pDXT5_block1->get_high_alpha());
for (uint i = 0; i < cDXTBlockSize * cDXTBlockSize; i++) {
const uint s0 = pDXT5_block0->get_selector(i & 3, i >> 2);
const uint s1 = pDXT5_block1->get_selector(i & 3, i >> 2);
if (crn_fmt == cCRNFmtDXN_XY)
pDst_pixels[i].set_noclamp_rgba(values0[s0], values1[s1], 255, 255);
else
pDst_pixels[i].set_noclamp_rgba(values1[s1], values0[s0], 255, 255);
}
break;
}
case cCRNFmtDXT5A: {
const dxt5_block* pDXT5_block = reinterpret_cast<const dxt5_block*>(pSrc_block);
uint values[cDXT5SelectorValues];
dxt5_block::get_block_values(values, pDXT5_block->get_low_alpha(), pDXT5_block->get_high_alpha());
for (uint i = 0; i < cDXTBlockSize * cDXTBlockSize; i++) {
const uint s = pDXT5_block->get_selector(i & 3, i >> 2);
pDst_pixels[i].set_noclamp_rgba(255, 255, 255, values[s]);
}
break;
}
default: {
return false;
}
}
return true;
}
crnlib.h
Replace with
// File: crnlib.h - Advanced DXTn texture compression library.
// Copyright (c) 2010-2016 Richard Geldreich, Jr. and Binomial LLC
// See copyright notice and license at the end of this file.
//
// This header file contains the public crnlib declarations for DXTn,
// clustered DXTn, and CRN compression/decompression.
//
// Note: This library does NOT need to be linked into your game executable if
// all you want to do is transcode .CRN files to raw DXTn bits at run-time.
// The crn_decomp.h header file library contains all the code necessary for
// decompression.
//
// Important: If compiling with gcc, be sure strict aliasing is disabled: -fno-strict-aliasing
#ifndef CRNLIB_H
#define CRNLIB_H
#ifdef _MSC_VER
#pragma warning(disable : 4127) // conditional expression is constant
#endif
#define CRNLIB_VERSION 104
#define CRNLIB_SUPPORT_ATI_COMPRESS 0
#define CRNLIB_SUPPORT_SQUISH 0
typedef unsigned char crn_uint8;
typedef unsigned short crn_uint16;
typedef unsigned int crn_uint32;
typedef signed char crn_int8;
typedef signed short crn_int16;
typedef signed int crn_int32;
typedef unsigned int crn_bool;
#ifdef TESTFUNCDLL_EXPORT
#define TESTFUNCDLL_API __declspec(dllexport)
#else
#define TESTFUNCDLL_API __declspec(dllimport)
#endif
// crnlib can compress to these file types.
enum crn_file_type {
// .CRN
cCRNFileTypeCRN = 0,
// .DDS using regular DXT or clustered DXT
cCRNFileTypeDDS,
cCRNFileTypeForceDWORD = 0xFFFFFFFF
};
// Supported compressed pixel formats.
// Basically all the standard DX9 formats, with some swizzled DXT5 formats
// (most of them supported by ATI's Compressonator), along with some ATI/X360 GPU specific formats.
enum crn_format {
cCRNFmtInvalid = -1,
cCRNFmtDXT1 = 0,
cCRNFmtFirstValid = cCRNFmtDXT1,
// cCRNFmtDXT3 is not currently supported when writing to CRN - only DDS.
cCRNFmtDXT3,
cCRNFmtDXT5,
// Various DXT5 derivatives
cCRNFmtDXT5_CCxY, // Luma-chroma
cCRNFmtDXT5_xGxR, // Swizzled 2-component
cCRNFmtDXT5_xGBR, // Swizzled 3-component
cCRNFmtDXT5_AGBR, // Swizzled 4-component
// ATI 3DC and X360 DXN
cCRNFmtDXN_XY,
cCRNFmtDXN_YX,
// DXT5 alpha blocks only
cCRNFmtDXT5A,
cCRNFmtETC1,
cCRNFmtETC2,
cCRNFmtETC2A,
cCRNFmtETC1S,
cCRNFmtETC2AS,
cCRNFmtTotal,
cCRNFmtForceDWORD = 0xFFFFFFFF
};
// Various library/file format limits.
enum crn_limits {
// Max. mipmap level resolution on any axis.
cCRNMaxLevelResolution = 4096,
cCRNMinPaletteSize = 8,
cCRNMaxPaletteSize = 8192,
cCRNMaxFaces = 6,
cCRNMaxLevels = 16,
cCRNMaxHelperThreads = 16,
cCRNMinQualityLevel = 0,
cCRNMaxQualityLevel = 255
};
// CRN/DDS compression flags.
// See the m_flags member in the crn_comp_params struct, below.
enum crn_comp_flags {
// Enables perceptual colorspace distance metrics if set.
// Important: Be sure to disable this when compressing non-sRGB colorspace images, like normal maps!
// Default: Set
cCRNCompFlagPerceptual = 1,
// Enables (up to) 8x8 macroblock usage if set. If disabled, only 4x4 blocks are allowed.
// Compression ratio will be lower when disabled, but may cut down on blocky artifacts because the process used to determine
// where large macroblocks can be used without artifacts isn't perfect.
// Default: Set.
cCRNCompFlagHierarchical = 2,
// cCRNCompFlagQuick disables several output file optimizations - intended for things like quicker previews.
// Default: Not set.
cCRNCompFlagQuick = 4,
// DXT1: OK to use DXT1 alpha blocks for better quality or DXT1A transparency.
// DXT5: OK to use both DXT5 block types.
// Currently only used when writing to .DDS files, as .CRN uses only a subset of the possible DXTn block types.
// Default: Set.
cCRNCompFlagUseBothBlockTypes = 8,
// OK to use DXT1A transparent indices to encode black (assumes pixel shader ignores fetched alpha).
// Currently only used when writing to .DDS files, .CRN never uses alpha blocks.
// Default: Not set.
cCRNCompFlagUseTransparentIndicesForBlack = 16,
// Disables endpoint caching, for more deterministic output.
// Currently only used when writing to .DDS files.
// Default: Not set.
cCRNCompFlagDisableEndpointCaching = 32,
// If enabled, use the cCRNColorEndpointPaletteSize, etc. params to control the CRN palette sizes. Only useful when writing to .CRN files.
// Default: Not set.
cCRNCompFlagManualPaletteSizes = 64,
// If enabled, DXT1A alpha blocks are used to encode single bit transparency.
// Default: Not set.
cCRNCompFlagDXT1AForTransparency = 128,
// If enabled, the DXT1 compressor's color distance metric assumes the pixel shader will be converting the fetched RGB results to luma (Y part of YCbCr).
// This increases quality when compressing grayscale images, because the compressor can spread the luma error amoung all three channels (i.e. it can generate blocks
// with some chroma present if doing so will ultimately lead to lower luma error).
// Only enable on grayscale source images.
// Default: Not set.
cCRNCompFlagGrayscaleSampling = 256,
// If enabled, debug information will be output during compression.
// Default: Not set.
cCRNCompFlagDebugging = 0x80000000,
cCRNCompFlagForceDWORD = 0xFFFFFFFF
};
// Controls DXTn quality vs. speed control - only used when compressing to .DDS.
enum crn_dxt_quality {
cCRNDXTQualitySuperFast,
cCRNDXTQualityFast,
cCRNDXTQualityNormal,
cCRNDXTQualityBetter,
cCRNDXTQualityUber,
cCRNDXTQualityTotal,
cCRNDXTQualityForceDWORD = 0xFFFFFFFF
};
// Which DXTn compressor to use when compressing to plain (non-clustered) .DDS.
enum crn_dxt_compressor_type {
cCRNDXTCompressorCRN, // Use crnlib's ETC1 or DXTc block compressor (default, highest quality, comparable or better than ati_compress or squish, and crnlib's ETC1 is a lot fasterw with similiar quality to Erricson's)
cCRNDXTCompressorCRNF, // Use crnlib's "fast" DXTc block compressor
cCRNDXTCompressorRYG, // Use RYG's DXTc block compressor (low quality, but very fast)
#if CRNLIB_SUPPORT_ATI_COMPRESS
cCRNDXTCompressorATI,
#endif
#if CRNLIB_SUPPORT_SQUISH
cCRNDXTCompressorSquish,
#endif
cCRNTotalDXTCompressors,
cCRNDXTCompressorForceDWORD = 0xFFFFFFFF
};
// Progress callback function.
// Processing will stop prematurely (and fail) if the callback returns false.
// phase_index, total_phases - high level progress
// subphase_index, total_subphases - progress within current phase
typedef crn_bool (*crn_progress_callback_func)(crn_uint32 phase_index, crn_uint32 total_phases, crn_uint32 subphase_index, crn_uint32 total_subphases, void* pUser_data_ptr);
// CRN/DDS compression parameters struct.
struct crn_comp_params {
inline crn_comp_params() { clear(); }
// Clear struct to default parameters.
inline void clear() {
m_size_of_obj = sizeof(*this);
m_file_type = cCRNFileTypeCRN;
m_faces = 1;
m_width = 0;
m_height = 0;
m_levels = 1;
m_format = cCRNFmtDXT1;
m_flags = cCRNCompFlagPerceptual | cCRNCompFlagHierarchical | cCRNCompFlagUseBothBlockTypes;
for (crn_uint32 f = 0; f < cCRNMaxFaces; f++)
for (crn_uint32 l = 0; l < cCRNMaxLevels; l++)
m_pImages[f][l] = NULL;
m_target_bitrate = 0.0f;
m_quality_level = cCRNMaxQualityLevel;
m_dxt1a_alpha_threshold = 128;
m_dxt_quality = cCRNDXTQualityUber;
m_dxt_compressor_type = cCRNDXTCompressorCRN;
m_alpha_component = 3;
m_crn_adaptive_tile_color_psnr_derating = 2.0f;
m_crn_adaptive_tile_alpha_psnr_derating = 2.0f;
m_crn_color_endpoint_palette_size = 0;
m_crn_color_selector_palette_size = 0;
m_crn_alpha_endpoint_palette_size = 0;
m_crn_alpha_selector_palette_size = 0;
m_num_helper_threads = 0;
m_userdata0 = 0;
m_userdata1 = 0;
m_pProgress_func = NULL;
m_pProgress_func_data = NULL;
}
inline bool operator==(const crn_comp_params& rhs) const {
#define CRNLIB_COMP(x) \
do { \
if ((x) != (rhs.x)) \
return false; \
} while (0)
CRNLIB_COMP(m_size_of_obj);
CRNLIB_COMP(m_file_type);
CRNLIB_COMP(m_faces);
CRNLIB_COMP(m_width);
CRNLIB_COMP(m_height);
CRNLIB_COMP(m_levels);
CRNLIB_COMP(m_format);
CRNLIB_COMP(m_flags);
CRNLIB_COMP(m_target_bitrate);
CRNLIB_COMP(m_quality_level);
CRNLIB_COMP(m_dxt1a_alpha_threshold);
CRNLIB_COMP(m_dxt_quality);
CRNLIB_COMP(m_dxt_compressor_type);
CRNLIB_COMP(m_alpha_component);
CRNLIB_COMP(m_crn_adaptive_tile_color_psnr_derating);
CRNLIB_COMP(m_crn_adaptive_tile_alpha_psnr_derating);
CRNLIB_COMP(m_crn_color_endpoint_palette_size);
CRNLIB_COMP(m_crn_color_selector_palette_size);
CRNLIB_COMP(m_crn_alpha_endpoint_palette_size);
CRNLIB_COMP(m_crn_alpha_selector_palette_size);
CRNLIB_COMP(m_num_helper_threads);
CRNLIB_COMP(m_userdata0);
CRNLIB_COMP(m_userdata1);
CRNLIB_COMP(m_pProgress_func);
CRNLIB_COMP(m_pProgress_func_data);
for (crn_uint32 f = 0; f < cCRNMaxFaces; f++)
for (crn_uint32 l = 0; l < cCRNMaxLevels; l++)
CRNLIB_COMP(m_pImages[f][l]);
#undef CRNLIB_COMP
return true;
}
// Returns true if the input parameters are reasonable.
inline bool check() const {
if ((m_file_type > cCRNFileTypeDDS) ||
(((int)m_quality_level < (int)cCRNMinQualityLevel) || ((int)m_quality_level > (int)cCRNMaxQualityLevel)) ||
(m_dxt1a_alpha_threshold > 255) ||
((m_faces != 1) && (m_faces != 6)) ||
((m_width < 1) || (m_width > cCRNMaxLevelResolution)) ||
((m_height < 1) || (m_height > cCRNMaxLevelResolution)) ||
((m_levels < 1) || (m_levels > cCRNMaxLevels)) ||
((m_format < cCRNFmtDXT1) || (m_format >= cCRNFmtTotal)) ||
((m_crn_color_endpoint_palette_size) && ((m_crn_color_endpoint_palette_size < cCRNMinPaletteSize) || (m_crn_color_endpoint_palette_size > cCRNMaxPaletteSize))) ||
((m_crn_color_selector_palette_size) && ((m_crn_color_selector_palette_size < cCRNMinPaletteSize) || (m_crn_color_selector_palette_size > cCRNMaxPaletteSize))) ||
((m_crn_alpha_endpoint_palette_size) && ((m_crn_alpha_endpoint_palette_size < cCRNMinPaletteSize) || (m_crn_alpha_endpoint_palette_size > cCRNMaxPaletteSize))) ||
((m_crn_alpha_selector_palette_size) && ((m_crn_alpha_selector_palette_size < cCRNMinPaletteSize) || (m_crn_alpha_selector_palette_size > cCRNMaxPaletteSize))) ||
(m_alpha_component > 3) ||
(m_num_helper_threads > cCRNMaxHelperThreads) ||
(m_dxt_quality > cCRNDXTQualityUber) ||
(m_dxt_compressor_type >= cCRNTotalDXTCompressors)) {
return false;
}
return true;
}
// Helper to set/get flags from m_flags member.
inline bool get_flag(crn_comp_flags flag) const { return (m_flags & flag) != 0; }
inline void set_flag(crn_comp_flags flag, bool val) {
m_flags &= ~flag;
if (val)
m_flags |= flag;
}
crn_uint32 m_size_of_obj;
crn_file_type m_file_type; // Output file type: cCRNFileTypeCRN or cCRNFileTypeDDS.
crn_uint32 m_faces; // 1 (2D map) or 6 (cubemap)
crn_uint32 m_width; // [1,cCRNMaxLevelResolution], non-power of 2 OK, non-square OK
crn_uint32 m_height; // [1,cCRNMaxLevelResolution], non-power of 2 OK, non-square OK
crn_uint32 m_levels; // [1,cCRNMaxLevelResolution], non-power of 2 OK, non-square OK
crn_format m_format; // Output pixel format.
crn_uint32 m_flags; // see crn_comp_flags enum
// Array of pointers to 32bpp input images.
const crn_uint32* m_pImages[cCRNMaxFaces][cCRNMaxLevels];
// Target bitrate - if non-zero, the compressor will use an interpolative search to find the
// highest quality level that is <= the target bitrate. If it fails to find a bitrate high enough, it'll
// try disabling adaptive block sizes (cCRNCompFlagHierarchical flag) and redo the search. This process can be pretty slow.
float m_target_bitrate;
// Desired quality level.
// Currently, CRN and DDS quality levels are not compatible with eachother from an image quality standpoint.
crn_uint32 m_quality_level; // [cCRNMinQualityLevel, cCRNMaxQualityLevel]
// DXTn compression parameters.
crn_uint32 m_dxt1a_alpha_threshold;
crn_dxt_quality m_dxt_quality;
crn_dxt_compressor_type m_dxt_compressor_type;
// Alpha channel's component. Defaults to 3.
crn_uint32 m_alpha_component;
// Various low-level CRN specific parameters.
float m_crn_adaptive_tile_color_psnr_derating;
float m_crn_adaptive_tile_alpha_psnr_derating;
crn_uint32 m_crn_color_endpoint_palette_size; // [cCRNMinPaletteSize,cCRNMaxPaletteSize]
crn_uint32 m_crn_color_selector_palette_size; // [cCRNMinPaletteSize,cCRNMaxPaletteSize]
crn_uint32 m_crn_alpha_endpoint_palette_size; // [cCRNMinPaletteSize,cCRNMaxPaletteSize]
crn_uint32 m_crn_alpha_selector_palette_size; // [cCRNMinPaletteSize,cCRNMaxPaletteSize]
// Number of helper threads to create during compression. 0=no threading.
crn_uint32 m_num_helper_threads;
// CRN userdata0 and userdata1 members, which are written directly to the header of the output file.
crn_uint32 m_userdata0;
crn_uint32 m_userdata1;
// User provided progress callback.
crn_progress_callback_func m_pProgress_func;
void* m_pProgress_func_data;
};
// Mipmap generator's mode.
enum crn_mip_mode {
cCRNMipModeUseSourceOrGenerateMips, // Use source texture's mipmaps if it has any, otherwise generate new mipmaps
cCRNMipModeUseSourceMips, // Use source texture's mipmaps if it has any, otherwise the output has no mipmaps
cCRNMipModeGenerateMips, // Always generate new mipmaps
cCRNMipModeNoMips, // Output texture has no mipmaps
cCRNMipModeTotal,
cCRNModeForceDWORD = 0xFFFFFFFF
};
const char* crn_get_mip_mode_desc(crn_mip_mode m);
const char* crn_get_mip_mode_name(crn_mip_mode m);
// Mipmap generator's filter kernel.
enum crn_mip_filter {
cCRNMipFilterBox,
cCRNMipFilterTent,
cCRNMipFilterLanczos4,
cCRNMipFilterMitchell,
cCRNMipFilterKaiser, // Kaiser=default mipmap filter
cCRNMipFilterTotal,
cCRNMipFilterForceDWORD = 0xFFFFFFFF
};
const char* crn_get_mip_filter_name(crn_mip_filter f);
// Mipmap generator's scale mode.
enum crn_scale_mode {
cCRNSMDisabled,
cCRNSMAbsolute,
cCRNSMRelative,
cCRNSMLowerPow2,
cCRNSMNearestPow2,
cCRNSMNextPow2,
cCRNSMTotal,
cCRNSMForceDWORD = 0xFFFFFFFF
};
const char* crn_get_scale_mode_desc(crn_scale_mode sm);
// Mipmap generator parameters.
struct crn_mipmap_params {
inline crn_mipmap_params() { clear(); }
inline void clear() {
m_size_of_obj = sizeof(*this);
m_mode = cCRNMipModeUseSourceOrGenerateMips;
m_filter = cCRNMipFilterKaiser;
m_gamma_filtering = true;
m_gamma = 2.2f;
// Default "blurriness" factor of .9 actually sharpens the output a little.
m_blurriness = .9f;
m_renormalize = false;
m_tiled = false;
m_max_levels = cCRNMaxLevels;
m_min_mip_size = 1;
m_scale_mode = cCRNSMDisabled;
m_scale_x = 1.0f;
m_scale_y = 1.0f;
m_window_left = 0;
m_window_top = 0;
m_window_right = 0;
m_window_bottom = 0;
m_clamp_scale = false;
m_clamp_width = 0;
m_clamp_height = 0;
}
inline bool check() const { return true; }
inline bool operator==(const crn_mipmap_params& rhs) const {
#define CRNLIB_COMP(x) \
do { \
if ((x) != (rhs.x)) \
return false; \
} while (0)
CRNLIB_COMP(m_size_of_obj);
CRNLIB_COMP(m_mode);
CRNLIB_COMP(m_filter);
CRNLIB_COMP(m_gamma_filtering);
CRNLIB_COMP(m_gamma);
CRNLIB_COMP(m_blurriness);
CRNLIB_COMP(m_renormalize);
CRNLIB_COMP(m_tiled);
CRNLIB_COMP(m_max_levels);
CRNLIB_COMP(m_min_mip_size);
CRNLIB_COMP(m_scale_mode);
CRNLIB_COMP(m_scale_x);
CRNLIB_COMP(m_scale_y);
CRNLIB_COMP(m_window_left);
CRNLIB_COMP(m_window_top);
CRNLIB_COMP(m_window_right);
CRNLIB_COMP(m_window_bottom);
CRNLIB_COMP(m_clamp_scale);
CRNLIB_COMP(m_clamp_width);
CRNLIB_COMP(m_clamp_height);
return true;
#undef CRNLIB_COMP
}
crn_uint32 m_size_of_obj;
crn_mip_mode m_mode;
crn_mip_filter m_filter;
crn_bool m_gamma_filtering;
float m_gamma;
float m_blurriness;
crn_uint32 m_max_levels;
crn_uint32 m_min_mip_size;
crn_bool m_renormalize;
crn_bool m_tiled;
crn_scale_mode m_scale_mode;
float m_scale_x;
float m_scale_y;
crn_uint32 m_window_left;
crn_uint32 m_window_top;
crn_uint32 m_window_right;
crn_uint32 m_window_bottom;
crn_bool m_clamp_scale;
crn_uint32 m_clamp_width;
crn_uint32 m_clamp_height;
};
// -------- High-level helper function definitions for CDN/DDS compression.
#ifndef CRNLIB_MIN_ALLOC_ALIGNMENT
#define CRNLIB_MIN_ALLOC_ALIGNMENT sizeof(size_t) * 2
#endif
// Function to set an optional user provided memory allocation/reallocation/msize routines.
// By default, crnlib just uses malloc(), free(), etc. for all allocations.
typedef void* (*crn_realloc_func)(void* p, size_t size, size_t* pActual_size, bool movable, void* pUser_data);
typedef size_t (*crn_msize_func)(void* p, void* pUser_data);
void crn_set_memory_callbacks(crn_realloc_func pRealloc, crn_msize_func pMSize, void* pUser_data);
#ifdef TESTFUNCDLL_EXPORT
#define TESTFUNCDLL_API __declspec(dllexport)
#else
#define TESTFUNCDLL_API __declspec(dllimport)
#endif
extern "C"
{
// Frees memory blocks allocated by crn_compress(), crn_decompress_crn_to_dds(), or crn_decompress_dds_to_images().
TESTFUNCDLL_API void crn_free_block(void* pBlock);
// Compresses a 32-bit/pixel texture to either: a regular DX9 DDS file, a "clustered" (or reduced entropy) DX9 DDS file, or a CRN file in memory.
// Input parameters:
// comp_params is the compression parameters struct, defined above.
// compressed_size will be set to the size of the returned memory block containing the output file.
// The returned block must be freed by calling crn_free_block().
// *pActual_quality_level will be set to the actual quality level used to compress the image. May be NULL.
// *pActual_bitrate will be set to the output file's effective bitrate, possibly taking into account LZMA compression. May be NULL.
// Return value:
// The compressed file data, or NULL on failure.
// compressed_size will be set to the size of the returned memory buffer.
// Notes:
// A "regular" DDS file is compressed using normal DXTn compression at the specified DXT quality level.
// A "clustered" DDS file is compressed using clustered DXTn compression to either the target bitrate or the specified integer quality factor.
// The output file is a standard DX9 format DDS file, except the compressor assumes you will be later losslessly compressing the DDS output file using the LZMA algorithm.
// A texture is defined as an array of 1 or 6 "faces" (6 faces=cubemap), where each "face" consists of between [1,cCRNMaxLevels] mipmap levels.
// Mipmap levels are simple 32-bit 2D images with a pitch of width*sizeof(uint32), arranged in the usual raster order (top scanline first).
// The image pixels may be grayscale (YYYX bytes in memory), grayscale/alpha (YYYA in memory), 24-bit (RGBX in memory), or 32-bit (RGBA) colors (where "X"=don't care).
// RGB color data is generally assumed to be in the sRGB colorspace. If not, be sure to clear the "cCRNCompFlagPerceptual" in the crn_comp_params struct!
TESTFUNCDLL_API void* crn_compress(crn_uint32* imagedata,
crn_uint32 width,
crn_uint32 height,
crn_uint32 format,
crn_uint32 flags,
crn_uint32 quality_level,
crn_uint32 worker_threads,
crn_bool create_mipmaps,
crn_uint32& compressed_size,
crn_uint32* pActual_quality_level,
float* pActual_bitrate);
TESTFUNCDLL_API void* crn_decompress(
const void* pCRN_file_data,
crn_uint32& file_size);
TESTFUNCDLL_API bool crn_decompress_dds_to_images(
const void* pDDS_file_data,
crn_uint32 dds_file_size,
crn_uint32** ppImages,
crn_uint32& faces,
crn_uint32& levels,
crn_uint32& width,
crn_uint32& height);
TESTFUNCDLL_API void crn_free_all_images(crn_uint32** ppImages, crn_uint32 faces, crn_uint32 levels);
}
void* crn_compress(const crn_comp_params& comp_params, crn_uint32& compressed_size, crn_uint32* pActual_quality_level = NULL, float* pActual_bitrate = NULL);
// Like the above function, except this function can also do things like generate mipmaps, and resize or crop the input texture before compression.
// The actual operations performed are controlled by the crn_mipmap_params struct members.
// Be sure to set the "m_gamma_filtering" member of crn_mipmap_params to false if the input texture is not sRGB.
void* crn_compress(const crn_comp_params& comp_params, const crn_mipmap_params& mip_params, crn_uint32& compressed_size, crn_uint32* pActual_quality_level = NULL, float* pActual_bitrate = NULL);
// Transcodes an entire CRN file to DDS using the crn_decomp.h header file library to do most of the heavy lifting.
// The output DDS file's format is guaranteed to be one of the DXTn formats in the crn_format enum.
// This is a fast operation, because the CRN format is explicitly designed to be efficiently transcodable to DXTn.
// For more control over decompression, see the lower-level helper functions in crn_decomp.h, which do not depend at all on crnlib.
void* crn_decompress_crn_to_dds(const void* pCRN_file_data, crn_uint32& file_size);
// Decompresses an entire DDS file in any supported format to uncompressed 32-bit/pixel image(s).
// See the crnlib::pixel_format enum in inc/dds_defs.h for a list of the supported DDS formats.
// You are responsible for freeing each image block, either by calling crn_free_all_images() or manually calling crn_free_block() on each image pointer.
struct crn_texture_desc {
crn_uint32 m_faces;
crn_uint32 m_width;
crn_uint32 m_height;
crn_uint32 m_levels;
crn_uint32 m_fmt_fourcc; // Same as crnlib::pixel_format
};
bool crn_decompress_dds_to_images(const void* pDDS_file_data, crn_uint32 dds_file_size, crn_uint32** ppImages, crn_texture_desc& tex_desc);
// -------- crn_format related helpers functions.
// Returns the FOURCC format equivalent to the specified crn_format.
crn_uint32 crn_get_format_fourcc(crn_format fmt);
// Returns the crn_format's bits per texel.
crn_uint32 crn_get_format_bits_per_texel(crn_format fmt);
// Returns the crn_format's number of bytes per block.
crn_uint32 crn_get_bytes_per_dxt_block(crn_format fmt);
// Returns the non-swizzled, basic DXTn version of the specified crn_format.
// This is the format you would supply D3D or OpenGL.
crn_format crn_get_fundamental_dxt_format(crn_format fmt);
// -------- String helpers.
// Converts a crn_file_type to a string.
const char* crn_get_file_type_ext(crn_file_type file_type);
// Converts a crn_format to a string.
const char* crn_get_format_string(crn_format fmt);
// Converts a crn_dxt_quality to a string.
const char* crn_get_dxt_quality_string(crn_dxt_quality q);
// -------- Low-level DXTn 4x4 block compressor API
// crnlib's DXTn endpoint optimizer actually supports any number of source pixels (i.e. from 1 to thousands, not just 16),
// but for simplicity this API only supports 4x4 texel blocks.
typedef void* crn_block_compressor_context_t;
// Create a DXTn block compressor.
// This function only supports the basic/nonswizzled "fundamental" formats: DXT1, DXT3, DXT5, DXT5A, DXN_XY and DXN_YX.
// Avoid calling this multiple times if you intend on compressing many blocks, because it allocates some memory.
crn_block_compressor_context_t crn_create_block_compressor(const crn_comp_params& params);
// Compresses a block of 16 pixels to the destination DXTn block.
// pDst_block should be 8 (for DXT1/DXT5A) or 16 bytes (all the others).
// pPixels should be an array of 16 crn_uint32's. Each crn_uint32 must be r,g,b,a (r is always first) in memory.
void crn_compress_block(crn_block_compressor_context_t pContext, const crn_uint32* pPixels, void* pDst_block);
// Frees a DXTn block compressor.
void crn_free_block_compressor(crn_block_compressor_context_t pContext);
// Unpacks a compressed block to pDst_pixels.
// pSrc_block should be 8 (for DXT1/DXT5A) or 16 bytes (all the others).
// pDst_pixel should be an array of 16 crn_uint32's. Each uint32 will be r,g,b,a (r is always first) in memory.
// crn_fmt should be one of the "fundamental" formats: DXT1, DXT3, DXT5, DXT5A, DXN_XY and DXN_YX.
// The various swizzled DXT5 formats (such as cCRNFmtDXT5_xGBR, etc.) will be unpacked as if they where plain DXT5.
// Returns false if the crn_fmt is invalid.
bool crn_decompress_block(const void* pSrc_block, crn_uint32* pDst_pixels, crn_format crn_fmt);
#endif // CRNLIB_H
//------------------------------------------------------------------------------
//
// crnlib uses the ZLIB license:
// http://opensource.org/licenses/Zlib
//
// Copyright (c) 2010-2016 Richard Geldreich, Jr. and Binomial LLC
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
//------------------------------------------------------------------------------
When building ensure you have the following configuration
Copy the crnlib.dll into your project Binaries/Win64 folder
To access crunch you need a c++ project which you need to add the following header anc cpp file.
Crunch.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/Texture2D.h"
class Crunch
{
public:
void LoadCRNLibrary();
void FreeCRNLibrary();
bool IsCRNLibraryLoaded() const;
// Crunch Methods
TArray<uint8> CrunchTexture(UTexture2D* Texture, const bool SRGB, const uint32 Quality, const bool CreateMipMaps, const bool Verbose) const;
TArray<uint8> CRN_To_DDS(TArray<uint8> CRNArray, bool Verbose) const;
static UTexture2D* LoadTextureDDS(TArray<uint8> DDSArray, bool SRGB, bool UseMipMaps, bool Verbose);
private:
// CRN LIB (Crunch) method binding definitions
typedef intptr_t* (*crn_compress_ptr)(uintptr_t* image, uint32 width, uint32, uint32 format, uint32 flags, uint32 quality, uint32 threads, uint32 create_mipmaps, uint32& compressed_size, uint32& quality_level, float& bitrate);
crn_compress_ptr crn_compress = nullptr;
typedef void (*crn_free_block_ptr)(intptr_t* ptr);
crn_free_block_ptr crn_free_block = nullptr;
typedef intptr_t* (*crn_decompress_ptr)(intptr_t* ptr, uint32& file_size);
crn_decompress_ptr crn_decompress = nullptr;
void* DLLHandle = nullptr;
static bool GetTextureData(UTexture2D* Texture, TArray<uint8>& OutArray);
static bool SetTextureData(UTexture2D* Texture, const TArray<uint32>& DXTData, EPixelFormat Format, const bool UseMipMaps);
static void GenerateMipMaps(UTexture2D* Texture, int32 MipCount);
static FString PixelFormatToString(EPixelFormat PixelFormat);
static EPixelFormat StringToPixelFormat(const FString& PixelFormat);
};
Crunch.cpp
#include "Crunch.h"
#include "Engine/Texture2D.h"
#include "Modules/ModuleManager.h"
// Public
/**
* Load the CRN Library DLL at runtime
* Note: IsCRNLibraryLoaded checks if the DLL was loaded
*/
void Crunch::LoadCRNLibrary()
{
FString DLLPath = FPaths::Combine(FPaths::ProjectDir(), TEXT("Binaries/Win64/crnlib.dll"));
DLLHandle = FPlatformProcess::GetDllHandle(*DLLPath);
if (DLLHandle)
{
crn_compress = static_cast<crn_compress_ptr>(FPlatformProcess::GetDllExport(DLLHandle, TEXT("crn_compress")));
crn_free_block = static_cast<crn_free_block_ptr>(FPlatformProcess::GetDllExport(DLLHandle, TEXT("crn_free_block")));
crn_decompress = static_cast<crn_decompress_ptr>(FPlatformProcess::GetDllExport(DLLHandle, TEXT("crn_decompress")));
UE_LOG(LogTemp, Log, TEXT("CRN LIB Bindings Successful"));
}
}
/**
* Unloads an already loaded CRN DLL
*/
void Crunch::FreeCRNLibrary()
{
if (DLLHandle)
{
FPlatformProcess::FreeDllHandle(DLLHandle);
DLLHandle = nullptr;
}
}
/**
* Is the CRN DLL loaded at runtime
* @return DLL Loaded = true, DLL Not Loaded = false
*/
bool Crunch::IsCRNLibraryLoaded() const
{
return DLLHandle ? true : false;
}
/**
* Crunch a texture of pixel format ( PF_DXT1, PF_DXT5, PF_BC5, PF_B8G8R8A8 )
* @param Texture - Source texture to use for the crunch compression
* @param SRGB - Texture space ( e.g. Albedo, Metallic, Specular SRGB=true, Normal map, AO, Packed Channels SRGB=false )
* @param Quality - Compress quality range ( 0 - Low, 128 - Medium, 255 - High )
* @param CreateMipMaps - Create mip map flag
* @param Verbose - Display log data of result
* @return Crunch byte array
*/
TArray<uint8> Crunch::CrunchTexture(UTexture2D* Texture, const bool SRGB, const uint32 Quality, const bool CreateMipMaps, const bool Verbose) const
{
TArray<uint8> SrcTextureArray;
EPixelFormat SrcPixelFormat = Texture->GetPixelFormat();
// Check supported texture formats ( PF_DXT1, PF_DXT5, PF_BC5, and PF_B8G8R8A8 (uncompressed) )
if (SrcPixelFormat != PF_DXT1 &&
SrcPixelFormat != PF_DXT5 &&
SrcPixelFormat != PF_BC5 &&
SrcPixelFormat != PF_B8G8R8A8)
{
if (Verbose)
{
UE_LOG(LogTemp, Error, TEXT("Invalid Crunch Texture Format : %s"), *PixelFormatToString(Texture->GetPixelFormat()));
}
return TArray<uint8>();
}
// CRN Formats
// 0 - DXT1
// 2 - DXT5
// 7 - BC5/ATI2
// SRGB Codes
// 10 - Non Linear
// 11 - Linear
uint32 Width = Texture->GetPlatformData()->SizeX;
uint32 Height = Texture->GetPlatformData()->SizeY;
uint32 Format = (SrcPixelFormat == PF_DXT1 || SrcPixelFormat == PF_B8G8R8A8) ? 0 : SrcPixelFormat == PF_DXT5 ? 2 : 7;
if (GetTextureData(Texture, SrcTextureArray))
{
// Allocate the unmanaged array
void* UnmanagedMemory = FMemory::Malloc(SrcTextureArray.Num());
// Initialize memory
FMemory::Memset(UnmanagedMemory, 0, SrcTextureArray.Num());
// Copy the byte array over to the unmanaged array
FMemory::Memcpy(UnmanagedMemory, SrcTextureArray.GetData(), SrcTextureArray.Num());
uint32 CompressionSize;
uint32 QualityLevel;
uint32 Threads = 16;
float Bitrate;
intptr_t* CRNDataPtr = crn_compress(
static_cast<uintptr_t*>(UnmanagedMemory),
Width,
Height,
Format,
SRGB ? 10 : 11,
Quality,
Threads,
CreateMipMaps ? 1 : 0,
CompressionSize,
QualityLevel,
Bitrate);
// Allocate space for the crunched byte array
TArray<uint8> CRNArray;
CRNArray.SetNum(CompressionSize);
// Copy the result into the result
FMemory::Memcpy(CRNArray.GetData(), CRNDataPtr, CRNArray.Num());
// Free the memory block
crn_free_block(CRNDataPtr);
// Free the memory
FMemory::Free(UnmanagedMemory);
if(Verbose)
{
UE_LOG(LogTemp, Log, TEXT("Crunch Texture - Format: %s Width: %d Height: %d SRGB: %s Source Size: %d (kb) CRN Size: %d (kb) Threads: %d Quality: %d Bit Rate: %f"),
*PixelFormatToString(SrcPixelFormat),
Width,
Height,
SRGB ? TEXT("true") : TEXT("false"),
SrcTextureArray.Num()/1024,
CompressionSize/1024,
Threads,
Quality,
Bitrate);
}
return CRNArray;
}
return TArray<uint8>();
}
/**
* Converts the CRN byte array into a DDS (DXT) byte array
* @param CRNArray - Crunch byte array
* @param Verbose - Show conversion results
* @return DDS byte array
*/
TArray<uint8> Crunch::CRN_To_DDS(TArray<uint8> CRNArray, bool Verbose) const
{
// Allocate the unmanaged array
void* UnmanagedMemory = FMemory::Malloc(CRNArray.Num());
// Copy the byte array over to the unmanaged array
FMemory::Memcpy(UnmanagedMemory, CRNArray.GetData(), CRNArray.Num());
uint32 DDSFileSize = CRNArray.Num();
// Get the DDS data from the CRN data
intptr_t* DDSDataPtr = crn_decompress(static_cast<intptr_t*>(UnmanagedMemory), DDSFileSize);
// Allocate space for the dds byte array
TArray<uint8> DDSArray;
DDSArray.SetNum(DDSFileSize);
// Copy the result into the result
FMemory::Memcpy(DDSArray.GetData(), DDSDataPtr, DDSArray.Num());
// Free the memory block
crn_free_block(DDSDataPtr);
// Free the memory
FMemory::Free(UnmanagedMemory);
if(Verbose)
{
UE_LOG(LogTemp, Log, TEXT("CRN to DDS - CRN Size: %d (kb) to DDS Size : %d (kb)"), CRNArray.Num()/1024, DDSArray.Num()/1024);
}
return DDSArray;
}
/**
* Load the texture from the DDS (DXT) byte array
* @param DDSArray - DDS byte array
* @param SRGB - Colorspace of the created texture
* @param UseMipMaps - Add the mip maps to the created texture when they exist
* @param Verbose - Show the created texture results
* @return The created DXT texture
*/
UTexture2D* Crunch::LoadTextureDDS(TArray<uint8> DDSArray, bool SRGB, bool UseMipMaps, bool Verbose)
{
// Discover the format from the DDS data
const FString formatStr = FString::Printf(TEXT("%c%c%c%c"),
static_cast<TCHAR>(DDSArray[84]),
static_cast<TCHAR>(DDSArray[85]),
static_cast<TCHAR>(DDSArray[86]),
static_cast<TCHAR>(DDSArray[87]));
// Supported formats from DDS are (DXT1, DXT5 or ATI2 (BC5))
if (formatStr == "DXT1" || formatStr == "DXT5" || formatStr == "ATI2")
{
// This header byte should be 124 for DDS image files
uint8 ddsSizeCheck = DDSArray[4];
if (ddsSizeCheck == 124)
{
int32 DDSWidth = DDSArray[17] * 256 + DDSArray[16];
int32 DDSHeight = DDSArray[13] * 256 + DDSArray[12];
int32 DDS_HEADER_SIZE = 128;
TArray<uint32> DXTArray;
DXTArray.SetNum(DDSArray.Num() - DDS_HEADER_SIZE);
size_t sourceOffset = DDS_HEADER_SIZE;
// Copy the DDS Array without its header ( this is the DXT Byte Array )
FMemory::Memcpy(DXTArray.GetData(), DDSArray.GetData() + sourceOffset, DXTArray.Num());
EPixelFormat PixelFormat;
if(formatStr == "DXT1")
PixelFormat = PF_DXT1;
else
if(formatStr == "DXT5")
PixelFormat = PF_DXT5;
else
PixelFormat = PF_BC5;
UTexture2D* Texture = UTexture2D::CreateTransient(DDSWidth, DDSHeight, PixelFormat);
Texture->SRGB = SRGB;
if(SetTextureData(Texture, DXTArray, PixelFormat, UseMipMaps))
{
if(Verbose)
{
UE_LOG(LogTemp, Log, TEXT("Load Texture DDS - Format: %s Width: %d Height: %d Use Mip Maps: %s"),
*formatStr, DDSWidth, DDSHeight, UseMipMaps ? TEXT("true" : TEXT("false")));
}
}
return Texture;
}
//UE_LOG(LogTemp, Warning, TEXT("Invalid DDS DXTn texture. Unable to read"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("Invalid DDS Format : %s Supports (DXT1, DXT5, ATI2 (BC5))"), *formatStr);
}
return nullptr;
}
// Private
/**
* Get the dxt pixel data from the texture and remaps the channels into an uint8 array.
* @param Texture - Compressed texture (DXT1, DXT5, BC5, B8G8R8A8)
* @param OutArray - Texture byte array
* @return true - success, false - fail
*/
bool Crunch::GetTextureData(UTexture2D* Texture, TArray<uint8>& OutArray)
{
if (!Texture || !Texture->GetPlatformData() || Texture->GetPlatformData()->Mips.Num() == 0)
{
UE_LOG(LogTemp, Error, TEXT("Invalid texture or texture data."));
return false;
}
if (Texture->GetPixelFormat() != PF_DXT1 &&
Texture->GetPixelFormat() != PF_DXT5 &&
Texture->GetPixelFormat() != PF_BC5 &&
Texture->GetPixelFormat() != PF_B8G8R8A8)
{
UE_LOG(LogTemp, Error, TEXT("Invalid texture format (requires DXT1,DXT5,BC5). %s"), *PixelFormatToString(Texture->GetPixelFormat()));
return false;
}
// Update texture with no mip maps and decompress (if required)
TextureCompressionSettings OldCompressionSettings = Texture->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = Texture->MipGenSettings;
Texture->CompressionSettings = TC_VectorDisplacementmap;
Texture->MipGenSettings = TMGS_NoMipmaps;
Texture->UpdateResource();
FTexturePlatformData* PlatformData = Texture->GetPlatformData();
// Get the texture data
FTexture2DMipMap& MipMap = PlatformData->Mips[0];
// Protect the textures data for reading
if (void* Data = MipMap.BulkData.Lock(LOCK_READ_ONLY))
{
int32 Width = PlatformData->SizeX;
int32 Height = PlatformData->SizeY;
// Allocate space for the texture data
OutArray.SetNum(Width * Height * 4);
// Get a pixel data byte pointer to the image
uint8* PixelData = static_cast<uint8*>(Data);
// Add the pixel data
for (int32 y = 0; y < Height; y++)
{
for (int32 x = 0; x < Width; x++)
{
int Index = (y * Width + x) * 4;
OutArray[Index] = PixelData[Index + 2]; // R
OutArray[Index + 1] = PixelData[Index + 1]; // G
OutArray[Index + 2] = PixelData[Index]; // B
OutArray[Index + 3] = PixelData[Index + 3]; // A
}
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Invalid data"));
}
MipMap.BulkData.Unlock();
// Restore the original texture mip gen settings
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->UpdateResource();
return true;
}
/**
* Set the dxt byte array into the dxt formatted texture
* @param Texture - Target texture
* @param DXTData - DXT data ( which may include its mip data if generated during crunch )
* @param Format - Supported Formats are PF_DXT1, PF_DXT5 and PF_BC5 (Normal Map)
* @param UseMipMaps - Add the mip maps if they exist in the dxt data
* @return true - success, false - fail
*/
bool Crunch::SetTextureData(UTexture2D* Texture, const TArray<uint32>& DXTData, EPixelFormat Format, const bool UseMipMaps)
{
if (!Texture)
{
UE_LOG(LogTemp, Error, TEXT("Invalid texture."));
return false;
}
FTexturePlatformData* PlatformData = Texture->GetPlatformData();
int32 TotalMipBytes = 0;
int32 Width = PlatformData->SizeX;
int32 Height = PlatformData->SizeY;
const int32 BlockSize = Format == PF_DXT1 ? 8 : 16; // DXT1 - 8 DXT5/BC5 - 16
/*if(Verbose)
{
UE_LOG(LogTemp, Error, TEXT("Format: %s Width: %d Height: %d DXT Array Size: %d bytes"),
*PixelFormatToString(Format), Width, Height, DXTData.Num());
for(int MipIndex=0; MipIndex<11; MipIndex++)
{
int32 MipSize = ((Width + 3) / 4) * ((Height + 3) / 4) * BlockSize;
TotalMipBytes += MipSize;
UE_LOG(LogTemp, Log, TEXT("Mip Index: %d Width: %d Height: %d Size: %d Total Mip Size: %d"), MipIndex, Width, Height, MipSize, TotalMipBytes);
Width = Width/2;
Height = Height/2;
}
}*/
/*TotalMipBytes = 0;
Width = PlatformData->SizeX;
Height = PlatformData->SizeY;*/
// Default compression settings for both DXT1 (BC1) and DXT5 (BC3 with A)
Texture->CompressionSettings = Format == PF_DXT1 ? TC_Default : TC_Normalmap;
Texture->MipGenSettings = TMGS_FromTextureGroup;
// Maintain a pointer to the dxt data, so we can advance to the next mip data
const uint32* CurrentPtr = DXTData.GetData();
// Copy the base DXT mip 0 data into the already allocated mip 0 in the texture
FTexture2DMipMap& Mip0 = Texture->GetPlatformData()->Mips[0];
const int32 Mip0Size = ((Width + 3) / 4) * ((Height + 3) / 4) * BlockSize;
TotalMipBytes += Mip0Size;
void* TextureData = Mip0.BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(TextureData, DXTData.GetData(), Mip0Size);
Mip0.BulkData.Unlock();
if(UseMipMaps)
{
constexpr int MaxMips = 11;
// Iterate through all other mips until none are found
for (int32 MipIndex=1; MipIndex<MaxMips; MipIndex++)
{
// Reduce the width and halve by halve
Width = Width/2;
Height = Height/2;
// Calculate the size of the mip
int32 MipSize = ((Width + 3) / 4) * ((Height + 3) / 4) * BlockSize;
// Check if we have another mip map to create
if (TotalMipBytes < DXTData.Num())
{
// Advance the pointer to the next mip level in the DXT array
CurrentPtr += MipSize;
// Create the mip
FTexture2DMipMap* Mip = new FTexture2DMipMap();
Mip->SizeX = Width;
Mip->SizeY = Height;
Texture->GetPlatformData()->Mips.Add(Mip);
Mip->BulkData.Lock(LOCK_READ_WRITE);
void* Mip1Data = Mip->BulkData.Realloc(MipSize);
FMemory::Memcpy(Mip1Data, CurrentPtr, MipSize);
Mip->BulkData.Unlock();
TotalMipBytes += MipSize;
}
else
{
// Break from the mip creation
break;
}
}
}
// Update the texture to load the mip data
Texture->UpdateResource();
return true;
}
/**
* Generate mip maps for an uncompressed texture
* Algorithm : Basic averaged down sampling
* Pixel format : PF_B8G8R8A8
*
* @param Texture - Texture to add mips for
* @param MipCount - Number of mip maps
*/
void Crunch::GenerateMipMaps(UTexture2D* Texture, const int32 MipCount)
{
// Check texture has data
if (!Texture || !Texture->GetPlatformData())
{
return;
}
Texture->CompressionSettings = TC_VectorDisplacementmap;
Texture->MipGenSettings = TMGS_NoMipmaps;
Texture->UpdateResource();
// Check texture format, compression and mip gen settings
if (Texture->GetPixelFormat() != PF_B8G8R8A8 ||
Texture->CompressionSettings != TC_VectorDisplacementmap ||
Texture->MipGenSettings != TMGS_NoMipmaps)
{
UE_LOG(LogTemp, Error, TEXT("Invalid texture format."));
return;
}
FTexturePlatformData* PlatformData = Texture->GetPlatformData();
// Assume we start with the top-level mip
int32 Width = PlatformData->SizeX;
int32 Height = PlatformData->SizeY;
// Create mip maps
for (int32 MipLevel = 1; MipLevel < MipCount; ++MipLevel)
{
Width = FMath::Max(1, Width / 2);
Height = FMath::Max(1, Height / 2);
// Allocate a mip map
FTexture2DMipMap* Mip = new FTexture2DMipMap();
Mip->SizeX = Width;
Mip->SizeY = Height;
PlatformData->Mips.Add(Mip);
// Calculate the size of the data - Assuming 4 bytes per pixel (32-bit RGBA)
const int32 DataSize = Width * Height * 4;
// Allocate and clear the mip data
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* MipData = Mip->BulkData.Realloc(DataSize);
FMemory::Memzero(MipData, DataSize);
// Sample from the previous mip level (MipLevel - 1)
const uint8* SrcMipData = static_cast<uint8*>(PlatformData->Mips[MipLevel - 1].BulkData.Lock(LOCK_READ_ONLY));
const int32 SrcWidth = PlatformData->Mips[MipLevel - 1].SizeX;
// Fill the mip data using simple averaged down sampling
for (int32 y = 0; y < Height; y++)
{
for (int32 x = 0; x < Width; x++)
{
// Calculate indices for the four source pixels
const int32 SrcIndex1 = (2 * y * SrcWidth + 2 * x) * 4;
const int32 SrcIndex2 = (2 * y * SrcWidth + 2 * x + 1) * 4;
const int32 SrcIndex3 = ((2 * y + 1) * SrcWidth + 2 * x) * 4;
const int32 SrcIndex4 = ((2 * y + 1) * SrcWidth + 2 * x + 1) * 4;
// Compute the average color
for (int32 c = 0; c < 4; c++)
{
MipData[(y * Width + x) * 4 + c] = (SrcMipData[SrcIndex1 + c] + SrcMipData[SrcIndex2 + c] + SrcMipData[SrcIndex3 + c] + SrcMipData[SrcIndex4 + c]) / 4;
}
}
}
// Unlock the bulk data memory
PlatformData->Mips[MipLevel - 1].BulkData.Unlock();
Mip->BulkData.Unlock();
}
// Update the texture resource
Texture->UpdateResource();
}
/**
* Convert enumerated texture format to string format
* @param PixelFormat - Texture format as enumeration
* @return Texture format as string
*/
FString Crunch::PixelFormatToString(EPixelFormat PixelFormat)
{
if (PixelFormat == PF_A32B32G32R32F)
return "PF_A32B32G32R32F";
if (PixelFormat == PF_B8G8R8A8)
return "PF_B8G8R8A8";
if (PixelFormat == PF_G8)
return "PF_G8";
if (PixelFormat == PF_G16)
return "PF_G16";
if (PixelFormat == PF_DXT1)
return "PF_DXT1";
if (PixelFormat == PF_DXT3)
return "PF_DXT3";
if (PixelFormat == PF_DXT5)
return "PF_DXT5";
if (PixelFormat == PF_BC4)
return "PF_BC4";
if (PixelFormat == PF_BC5)
return "PF_BC5";
if (PixelFormat == PF_R32_FLOAT)
return "PF_R32_FLOAT";
if (PixelFormat == PF_R16G16B16A16_UINT)
return "PF_R16G16B16A16_UINT";
if (PixelFormat == PF_R8G8)
return "PF_R8G8";
if (PixelFormat == PF_R8G8B8A8)
return "PF_R8G8B8A8";
if (PixelFormat == PF_A2B10G10R10)
return "PF_A2B10G10R10";
return "PF_Unknown";
}
/**
* Convert texture string format to enumeration format
* @param PixelFormat - Texture format as string
* @return Texture format as enumeration
*/
EPixelFormat Crunch::StringToPixelFormat(const FString& PixelFormat)
{
if (PixelFormat == "PF_A32B32G32R32F")
return PF_A32B32G32R32F;
if (PixelFormat == "PF_B8G8R8A8")
return PF_B8G8R8A8;
if (PixelFormat == "PF_G8")
return PF_G8;
if (PixelFormat == "PF_G16")
return PF_G16;
if (PixelFormat == "PF_DXT1")
return PF_DXT1;
if (PixelFormat == "PF_DXT3")
return PF_DXT3;
if (PixelFormat == "PF_DXT5")
return PF_DXT5;
if (PixelFormat == "PF_BC4")
return PF_BC4;
if (PixelFormat == "PF_BC5")
return PF_BC5;
if (PixelFormat == "PF_R32_FLOAT")
return PF_R32_FLOAT;
if (PixelFormat == "PF_R16G16B16A16_UINT")
return PF_R16G16B16A16_UINT;
if (PixelFormat == "PF_R8G8")
return PF_R8G8;
if (PixelFormat == "PF_R8G8B8A8")
return PF_R8G8B8A8;
if (PixelFormat == "PF_A2B10G10R10")
return PF_A2B10G10R10;
return PF_Unknown;
}
Testing Crunch Library
In your header file for your actor you will need the following
#pragma once
#include "CoreMinimal.h"
#include "Crunch.h"
...
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom")
UTexture2D* SrcTexture;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom")
UTexture2D* TargetTexture;
private:
Crunch CRNLib;
In your BeginPlay() method
Super::BeginPlay();
CRNLib.LoadCRNLibrary();
if(CRNLib.IsCRNLibraryLoaded())
{
bool Verbose = true;
bool SRGB = SrcTexture->SRGB;
bool CreateMipMaps = true;
uint32 Quality = 128;
TArray<uint8> CRNArray = CRNLib.CrunchTexture(SrcTexture, SRGB, Quality, CreateMipMaps, Verbose);
if(!CRNArray.IsEmpty())
{
TArray<uint8> DDSArray = CRNLib.CRN_To_DDS(CRNArray, Verbose);
TargetTexture = CRNLib.LoadTextureDDS(DDSArray, SRGB, CreateMipMaps, Verbose);
}
}
In your EndPlay() method
Super::EndPlay(EndPlayReason);
CRNLib.FreeCRNLibrary();