/********************************************************************************************** * * rtextures - Basic functions to load and draw textures * * CONFIGURATION: * * #define SUPPORT_MODULE_RTEXTURES * rtextures module is included in the build * * #define SUPPORT_FILEFORMAT_BMP * #define SUPPORT_FILEFORMAT_PNG * #define SUPPORT_FILEFORMAT_TGA * #define SUPPORT_FILEFORMAT_JPG * #define SUPPORT_FILEFORMAT_GIF * #define SUPPORT_FILEFORMAT_QOI * #define SUPPORT_FILEFORMAT_PSD * #define SUPPORT_FILEFORMAT_PIC * #define SUPPORT_FILEFORMAT_HDR * #define SUPPORT_FILEFORMAT_DDS * #define SUPPORT_FILEFORMAT_PKM * #define SUPPORT_FILEFORMAT_KTX * #define SUPPORT_FILEFORMAT_PVR * #define SUPPORT_FILEFORMAT_ASTC * Select desired fileformats to be supported for image data loading. Some of those formats are * supported by default, to remove support, just comment unrequired #define in this module * * #define SUPPORT_IMAGE_EXPORT * Support image export in multiple file formats * * #define SUPPORT_IMAGE_MANIPULATION * Support multiple image editing functions to scale, adjust colors, flip, draw on images, crop... * If not defined only three image editing functions supported: ImageFormat(), ImageAlphaMask(), ImageToPOT() * * #define SUPPORT_IMAGE_GENERATION * Support procedural image generation functionality (gradient, spot, perlin-noise, cellular) * * DEPENDENCIES: * stb_image - Multiple image formats loading (JPEG, PNG, BMP, TGA, PSD, GIF, PIC) * NOTE: stb_image has been slightly modified to support Android platform. * stb_image_resize - Multiple image resize algorythms * * * LICENSE: zlib/libpng * * Copyright (c) 2013-2022 Ramon Santamaria (@raysan5) * * 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. * **********************************************************************************************/ #include "raylib.h" // Declares module functions // Check if config flags have been externally provided on compilation line #if !defined(EXTERNAL_CONFIG_FLAGS) #include "config.h" // Defines module configuration flags #endif #if defined(SUPPORT_MODULE_RTEXTURES) #include "utils.h" // Required for: TRACELOG() and fopen() Android mapping #include "rlgl.h" // OpenGL abstraction layer to OpenGL 1.1, 3.3 or ES2 #include // Required for: malloc(), free() #include // Required for: strlen() [Used in ImageTextEx()], strcmp() [Used in LoadImageFromMemory()] #include // Required for: fabsf() #include // Required for: sprintf() [Used in ExportImageAsCode()] // Support only desired texture formats on stb_image #if !defined(SUPPORT_FILEFORMAT_BMP) #define STBI_NO_BMP #endif #if !defined(SUPPORT_FILEFORMAT_PNG) #define STBI_NO_PNG #endif #if !defined(SUPPORT_FILEFORMAT_TGA) #define STBI_NO_TGA #endif #if !defined(SUPPORT_FILEFORMAT_JPG) #define STBI_NO_JPEG // Image format .jpg and .jpeg #endif #if !defined(SUPPORT_FILEFORMAT_PSD) #define STBI_NO_PSD #endif #if !defined(SUPPORT_FILEFORMAT_GIF) #define STBI_NO_GIF #endif #if !defined(SUPPORT_FILEFORMAT_PIC) #define STBI_NO_PIC #endif #if !defined(SUPPORT_FILEFORMAT_HDR) #define STBI_NO_HDR #endif // Image fileformats not supported by default #define STBI_NO_PIC #define STBI_NO_PNM // Image format .ppm and .pgm #if defined(__TINYC__) #define STBI_NO_SIMD #endif #if (defined(SUPPORT_FILEFORMAT_BMP) || \ defined(SUPPORT_FILEFORMAT_PNG) || \ defined(SUPPORT_FILEFORMAT_TGA) || \ defined(SUPPORT_FILEFORMAT_JPG) || \ defined(SUPPORT_FILEFORMAT_PSD) || \ defined(SUPPORT_FILEFORMAT_GIF) || \ defined(SUPPORT_FILEFORMAT_PIC) || \ defined(SUPPORT_FILEFORMAT_HDR)) #define STBI_MALLOC RL_MALLOC #define STBI_FREE RL_FREE #define STBI_REALLOC RL_REALLOC #define STB_IMAGE_IMPLEMENTATION #include "external/stb_image.h" // Required for: stbi_load_from_file() // NOTE: Used to read image data (multiple formats support) #endif #if defined(SUPPORT_FILEFORMAT_QOI) #define QOI_MALLOC RL_MALLOC #define QOI_FREE RL_FREE #define QOI_IMPLEMENTATION #include "external/qoi.h" #endif #if defined(SUPPORT_IMAGE_EXPORT) #define STBIW_MALLOC RL_MALLOC #define STBIW_FREE RL_FREE #define STBIW_REALLOC RL_REALLOC #define STB_IMAGE_WRITE_IMPLEMENTATION #include "external/stb_image_write.h" // Required for: stbi_write_*() #endif #if defined(SUPPORT_IMAGE_MANIPULATION) #define STBIR_MALLOC(size,c) ((void)(c), RL_MALLOC(size)) #define STBIR_FREE(ptr,c) ((void)(c), RL_FREE(ptr)) #define STB_IMAGE_RESIZE_IMPLEMENTATION #include "external/stb_image_resize.h" // Required for: stbir_resize_uint8() [ImageResize()] #endif //---------------------------------------------------------------------------------- // Defines and Macros //---------------------------------------------------------------------------------- #ifndef PIXELFORMAT_UNCOMPRESSED_R5G5B5A1_ALPHA_THRESHOLD #define PIXELFORMAT_UNCOMPRESSED_R5G5B5A1_ALPHA_THRESHOLD 50 // Threshold over 255 to set alpha as 0 #endif //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- // ... //---------------------------------------------------------------------------------- // Global Variables Definition //---------------------------------------------------------------------------------- // It's lonely here... //---------------------------------------------------------------------------------- // Other Modules Functions Declaration (required by text) //---------------------------------------------------------------------------------- // ... //---------------------------------------------------------------------------------- // Module specific Functions Declaration //---------------------------------------------------------------------------------- #if defined(SUPPORT_FILEFORMAT_DDS) static Image LoadDDS(const unsigned char *fileData, unsigned int fileSize); // Load DDS file data #endif #if defined(SUPPORT_FILEFORMAT_PKM) static Image LoadPKM(const unsigned char *fileData, unsigned int fileSize); // Load PKM file data #endif #if defined(SUPPORT_FILEFORMAT_KTX) static Image LoadKTX(const unsigned char *fileData, unsigned int fileSize); // Load KTX file data static int SaveKTX(Image image, const char *fileName); // Save image data as KTX file #endif #if defined(SUPPORT_FILEFORMAT_PVR) static Image LoadPVR(const unsigned char *fileData, unsigned int fileSize); // Load PVR file data #endif #if defined(SUPPORT_FILEFORMAT_ASTC) static Image LoadASTC(const unsigned char *fileData, unsigned int fileSize); // Load ASTC file data #endif static Vector4 *LoadImageDataNormalized(Image image); // Load pixel data from image as Vector4 array (float normalized) //---------------------------------------------------------------------------------- // Module Functions Definition //---------------------------------------------------------------------------------- // Load image from file into CPU memory (RAM) Image LoadImage(const char *fileName) { Image image = { 0 }; #if defined(SUPPORT_FILEFORMAT_PNG) || \ defined(SUPPORT_FILEFORMAT_BMP) || \ defined(SUPPORT_FILEFORMAT_TGA) || \ defined(SUPPORT_FILEFORMAT_JPG) || \ defined(SUPPORT_FILEFORMAT_GIF) || \ defined(SUPPORT_FILEFORMAT_PIC) || \ defined(SUPPORT_FILEFORMAT_HDR) || \ defined(SUPPORT_FILEFORMAT_PSD) #define STBI_REQUIRED #endif // Loading file to memory unsigned int fileSize = 0; unsigned char *fileData = LoadFileData(fileName, &fileSize); // Loading image from memory data if (fileData != NULL) image = LoadImageFromMemory(GetFileExtension(fileName), fileData, fileSize); RL_FREE(fileData); return image; } // Load an image from RAW file data Image LoadImageRaw(const char *fileName, int width, int height, int format, int headerSize) { Image image = { 0 }; unsigned int dataSize = 0; unsigned char *fileData = LoadFileData(fileName, &dataSize); if (fileData != NULL) { unsigned char *dataPtr = fileData; unsigned int size = GetPixelDataSize(width, height, format); if (headerSize > 0) dataPtr += headerSize; image.data = RL_MALLOC(size); // Allocate required memory in bytes memcpy(image.data, dataPtr, size); // Copy required data to image image.width = width; image.height = height; image.mipmaps = 1; image.format = format; RL_FREE(fileData); } return image; } // Load animated image data // - Image.data buffer includes all frames: [image#0][image#1][image#2][...] // - Number of frames is returned through 'frames' parameter // - All frames are returned in RGBA format // - Frames delay data is discarded Image LoadImageAnim(const char *fileName, int *frames) { Image image = { 0 }; int frameCount = 1; #if defined(SUPPORT_FILEFORMAT_GIF) if (IsFileExtension(fileName, ".gif")) { unsigned int dataSize = 0; unsigned char *fileData = LoadFileData(fileName, &dataSize); if (fileData != NULL) { int comp = 0; int *delays = NULL; image.data = stbi_load_gif_from_memory(fileData, dataSize, &delays, &image.width, &image.height, &frameCount, &comp, 4); image.mipmaps = 1; image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; RL_FREE(fileData); RL_FREE(delays); // NOTE: Frames delays are discarded } } #else if (false) { } #endif else image = LoadImage(fileName); // TODO: Support APNG animated images *frames = frameCount; return image; } // Load image from memory buffer, fileType refers to extension: i.e. ".png" // WARNING: File extension must be provided in lower-case Image LoadImageFromMemory(const char *fileType, const unsigned char *fileData, int dataSize) { Image image = { 0 }; #if defined(SUPPORT_FILEFORMAT_PNG) if ((strcmp(fileType, ".png") == 0) #else if ((false) #endif #if defined(SUPPORT_FILEFORMAT_BMP) || (strcmp(fileType, ".bmp") == 0) #endif #if defined(SUPPORT_FILEFORMAT_TGA) || (strcmp(fileType, ".tga") == 0) #endif #if defined(SUPPORT_FILEFORMAT_JPG) || ((strcmp(fileType, ".jpg") == 0) || (strcmp(fileType, ".jpeg") == 0)) #endif #if defined(SUPPORT_FILEFORMAT_GIF) || (strcmp(fileType, ".gif") == 0) #endif #if defined(SUPPORT_FILEFORMAT_PIC) || (strcmp(fileType, ".pic") == 0) #endif #if defined(SUPPORT_FILEFORMAT_PSD) || (strcmp(fileType, ".psd") == 0) #endif ) { #if defined(STBI_REQUIRED) // NOTE: Using stb_image to load images (Supports multiple image formats) if (fileData != NULL) { int comp = 0; image.data = stbi_load_from_memory(fileData, dataSize, &image.width, &image.height, &comp, 0); if (image.data != NULL) { image.mipmaps = 1; if (comp == 1) image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE; else if (comp == 2) image.format = PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA; else if (comp == 3) image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8; else if (comp == 4) image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; } } #endif } #if defined(SUPPORT_FILEFORMAT_HDR) else if (strcmp(fileType, ".hdr") == 0) { #if defined(STBI_REQUIRED) if (fileData != NULL) { int comp = 0; image.data = stbi_loadf_from_memory(fileData, dataSize, &image.width, &image.height, &comp, 0); image.mipmaps = 1; if (comp == 1) image.format = PIXELFORMAT_UNCOMPRESSED_R32; else if (comp == 3) image.format = PIXELFORMAT_UNCOMPRESSED_R32G32B32; else if (comp == 4) image.format = PIXELFORMAT_UNCOMPRESSED_R32G32B32A32; else { TRACELOG(LOG_WARNING, "IMAGE: HDR file format not supported"); UnloadImage(image); } } #endif } #endif #if defined(SUPPORT_FILEFORMAT_QOI) else if (strcmp(fileType, ".qoi") == 0) { qoi_desc desc = { 0 }; image.data = qoi_decode(fileData, dataSize, &desc, 4); image.width = desc.width; image.height = desc.height; image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; image.mipmaps = 1; } #endif #if defined(SUPPORT_FILEFORMAT_DDS) else if (strcmp(fileType, ".dds") == 0) image = LoadDDS(fileData, dataSize); #endif #if defined(SUPPORT_FILEFORMAT_PKM) else if (strcmp(fileType, ".pkm") == 0) image = LoadPKM(fileData, dataSize); #endif #if defined(SUPPORT_FILEFORMAT_KTX) else if (strcmp(fileType, ".ktx") == 0) image = LoadKTX(fileData, dataSize); #endif #if defined(SUPPORT_FILEFORMAT_PVR) else if (strcmp(fileType, ".pvr") == 0) image = LoadPVR(fileData, dataSize); #endif #if defined(SUPPORT_FILEFORMAT_ASTC) else if (strcmp(fileType, ".astc") == 0) image = LoadASTC(fileData, dataSize); #endif else TRACELOG(LOG_WARNING, "IMAGE: Data format not supported"); if (image.data != NULL) TRACELOG(LOG_INFO, "IMAGE: Data loaded successfully (%ix%i | %s | %i mipmaps)", image.width, image.height, rlGetPixelFormatName(image.format), image.mipmaps); else TRACELOG(LOG_WARNING, "IMAGE: Failed to load image data"); return image; } // Load image from GPU texture data // NOTE: Compressed texture formats not supported Image LoadImageFromTexture(Texture2D texture) { Image image = { 0 }; if (texture.format < PIXELFORMAT_COMPRESSED_DXT1_RGB) { image.data = rlReadTexturePixels(texture.id, texture.width, texture.height, texture.format); if (image.data != NULL) { image.width = texture.width; image.height = texture.height; image.format = texture.format; image.mipmaps = 1; #if defined(GRAPHICS_API_OPENGL_ES2) // NOTE: Data retrieved on OpenGL ES 2.0 should be RGBA, // coming from FBO color buffer attachment, but it seems // original texture format is retrieved on RPI... image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; #endif TRACELOG(LOG_INFO, "TEXTURE: [ID %i] Pixel data retrieved successfully", texture.id); } else TRACELOG(LOG_WARNING, "TEXTURE: [ID %i] Failed to retrieve pixel data", texture.id); } else TRACELOG(LOG_WARNING, "TEXTURE: [ID %i] Failed to retrieve compressed pixel data", texture.id); return image; } // Load image from screen buffer and (screenshot) Image LoadImageFromScreen(void) { Image image = { 0 }; image.width = GetScreenWidth(); image.height = GetScreenHeight(); image.mipmaps = 1; image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; image.data = rlReadScreenPixels(image.width, image.height); return image; } // Unload image from CPU memory (RAM) void UnloadImage(Image image) { RL_FREE(image.data); } // Export image data to file // NOTE: File format depends on fileName extension bool ExportImage(Image image, const char *fileName) { int success = 0; #if defined(SUPPORT_IMAGE_EXPORT) int channels = 4; bool allocatedData = false; unsigned char *imgData = (unsigned char *)image.data; if (image.format == PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) channels = 1; else if (image.format == PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA) channels = 2; else if (image.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8) channels = 3; else if (image.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) channels = 4; else { // NOTE: Getting Color array as RGBA unsigned char values imgData = (unsigned char *)LoadImageColors(image); allocatedData = true; } #if defined(SUPPORT_FILEFORMAT_PNG) if (IsFileExtension(fileName, ".png")) { int dataSize = 0; unsigned char *fileData = stbi_write_png_to_mem((const unsigned char *)imgData, image.width*channels, image.width, image.height, channels, &dataSize); success = SaveFileData(fileName, fileData, dataSize); RL_FREE(fileData); } #else if (false) { } #endif #if defined(SUPPORT_FILEFORMAT_BMP) else if (IsFileExtension(fileName, ".bmp")) success = stbi_write_bmp(fileName, image.width, image.height, channels, imgData); #endif #if defined(SUPPORT_FILEFORMAT_TGA) else if (IsFileExtension(fileName, ".tga")) success = stbi_write_tga(fileName, image.width, image.height, channels, imgData); #endif #if defined(SUPPORT_FILEFORMAT_JPG) else if (IsFileExtension(fileName, ".jpg") || IsFileExtension(fileName, ".jpeg")) success = stbi_write_jpg(fileName, image.width, image.height, channels, imgData, 90); // JPG quality: between 1 and 100 #endif #if defined(SUPPORT_FILEFORMAT_QOI) else if (IsFileExtension(fileName, ".qoi")) { channels = 0; if (image.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8) channels = 3; else if (image.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) channels = 4; else TRACELOG(LOG_WARNING, "IMAGE: Image pixel format must be R8G8B8 or R8G8B8A8"); if ((channels == 3) || (channels == 4)) { qoi_desc desc = { 0 }; desc.width = image.width; desc.height = image.height; desc.channels = channels; desc.colorspace = QOI_SRGB; success = qoi_write(fileName, imgData, &desc); } } #endif #if defined(SUPPORT_FILEFORMAT_KTX) else if (IsFileExtension(fileName, ".ktx")) success = SaveKTX(image, fileName); #endif else if (IsFileExtension(fileName, ".raw")) { // Export raw pixel data (without header) // NOTE: It's up to the user to track image parameters success = SaveFileData(fileName, image.data, GetPixelDataSize(image.width, image.height, image.format)); } if (allocatedData) RL_FREE(imgData); #endif // SUPPORT_IMAGE_EXPORT if (success != 0) TRACELOG(LOG_INFO, "FILEIO: [%s] Image exported successfully", fileName); else TRACELOG(LOG_WARNING, "FILEIO: [%s] Failed to export image", fileName); return success; } // Export image as code file (.h) defining an array of bytes bool ExportImageAsCode(Image image, const char *fileName) { bool success = false; #if defined(SUPPORT_IMAGE_EXPORT) #ifndef TEXT_BYTES_PER_LINE #define TEXT_BYTES_PER_LINE 20 #endif int dataSize = GetPixelDataSize(image.width, image.height, image.format); // NOTE: Text data buffer size is estimated considering image data size in bytes // and requiring 6 char bytes for every byte: "0x00, " char *txtData = (char *)RL_CALLOC(dataSize*6 + 2000, sizeof(char)); int byteCount = 0; byteCount += sprintf(txtData + byteCount, "////////////////////////////////////////////////////////////////////////////////////////\n"); byteCount += sprintf(txtData + byteCount, "// //\n"); byteCount += sprintf(txtData + byteCount, "// ImageAsCode exporter v1.0 - Image pixel data exported as an array of bytes //\n"); byteCount += sprintf(txtData + byteCount, "// //\n"); byteCount += sprintf(txtData + byteCount, "// more info and bugs-report: github.com/raysan5/raylib //\n"); byteCount += sprintf(txtData + byteCount, "// feedback and support: ray[at]raylib.com //\n"); byteCount += sprintf(txtData + byteCount, "// //\n"); byteCount += sprintf(txtData + byteCount, "// Copyright (c) 2018-2022 Ramon Santamaria (@raysan5) //\n"); byteCount += sprintf(txtData + byteCount, "// //\n"); byteCount += sprintf(txtData + byteCount, "////////////////////////////////////////////////////////////////////////////////////////\n\n"); // Get file name from path and convert variable name to uppercase char varFileName[256] = { 0 }; strcpy(varFileName, GetFileNameWithoutExt(fileName)); for (int i = 0; varFileName[i] != '\0'; i++) if ((varFileName[i] >= 'a') && (varFileName[i] <= 'z')) { varFileName[i] = varFileName[i] - 32; } // Add image information byteCount += sprintf(txtData + byteCount, "// Image data information\n"); byteCount += sprintf(txtData + byteCount, "#define %s_WIDTH %i\n", varFileName, image.width); byteCount += sprintf(txtData + byteCount, "#define %s_HEIGHT %i\n", varFileName, image.height); byteCount += sprintf(txtData + byteCount, "#define %s_FORMAT %i // raylib internal pixel format\n\n", varFileName, image.format); byteCount += sprintf(txtData + byteCount, "static unsigned char %s_DATA[%i] = { ", varFileName, dataSize); for (int i = 0; i < dataSize - 1; i++) byteCount += sprintf(txtData + byteCount, ((i%TEXT_BYTES_PER_LINE == 0)? "0x%x,\n" : "0x%x, "), ((unsigned char *)image.data)[i]); byteCount += sprintf(txtData + byteCount, "0x%x };\n", ((unsigned char *)image.data)[dataSize - 1]); // NOTE: Text data size exported is determined by '\0' (NULL) character success = SaveFileText(fileName, txtData); RL_FREE(txtData); #endif // SUPPORT_IMAGE_EXPORT if (success != 0) TRACELOG(LOG_INFO, "FILEIO: [%s] Image as code exported successfully", fileName); else TRACELOG(LOG_WARNING, "FILEIO: [%s] Failed to export image as code", fileName); return success; } //------------------------------------------------------------------------------------ // Image generation functions //------------------------------------------------------------------------------------ // Generate image: plain color Image GenImageColor(int width, int height, Color color) { Color *pixels = (Color *)RL_CALLOC(width*height, sizeof(Color)); for (int i = 0; i < width*height; i++) pixels[i] = color; Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } #if defined(SUPPORT_IMAGE_GENERATION) // Generate image: vertical gradient Image GenImageGradientV(int width, int height, Color top, Color bottom) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); for (int j = 0; j < height; j++) { float factor = (float)j/(float)height; for (int i = 0; i < width; i++) { pixels[j*width + i].r = (int)((float)bottom.r*factor + (float)top.r*(1.f - factor)); pixels[j*width + i].g = (int)((float)bottom.g*factor + (float)top.g*(1.f - factor)); pixels[j*width + i].b = (int)((float)bottom.b*factor + (float)top.b*(1.f - factor)); pixels[j*width + i].a = (int)((float)bottom.a*factor + (float)top.a*(1.f - factor)); } } Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } // Generate image: horizontal gradient Image GenImageGradientH(int width, int height, Color left, Color right) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); for (int i = 0; i < width; i++) { float factor = (float)i/(float)width; for (int j = 0; j < height; j++) { pixels[j*width + i].r = (int)((float)right.r*factor + (float)left.r*(1.f - factor)); pixels[j*width + i].g = (int)((float)right.g*factor + (float)left.g*(1.f - factor)); pixels[j*width + i].b = (int)((float)right.b*factor + (float)left.b*(1.f - factor)); pixels[j*width + i].a = (int)((float)right.a*factor + (float)left.a*(1.f - factor)); } } Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } // Generate image: radial gradient Image GenImageGradientRadial(int width, int height, float density, Color inner, Color outer) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); float radius = (width < height)? (float)width/2.0f : (float)height/2.0f; float centerX = (float)width/2.0f; float centerY = (float)height/2.0f; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { float dist = hypotf((float)x - centerX, (float)y - centerY); float factor = (dist - radius*density)/(radius*(1.0f - density)); factor = (float)fmax(factor, 0.0f); factor = (float)fmin(factor, 1.f); // dist can be bigger than radius so we have to check pixels[y*width + x].r = (int)((float)outer.r*factor + (float)inner.r*(1.0f - factor)); pixels[y*width + x].g = (int)((float)outer.g*factor + (float)inner.g*(1.0f - factor)); pixels[y*width + x].b = (int)((float)outer.b*factor + (float)inner.b*(1.0f - factor)); pixels[y*width + x].a = (int)((float)outer.a*factor + (float)inner.a*(1.0f - factor)); } } Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } // Generate image: checked Image GenImageChecked(int width, int height, int checksX, int checksY, Color col1, Color col2) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if ((x/checksX + y/checksY)%2 == 0) pixels[y*width + x] = col1; else pixels[y*width + x] = col2; } } Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } // Generate image: white noise Image GenImageWhiteNoise(int width, int height, float factor) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); for (int i = 0; i < width*height; i++) { if (GetRandomValue(0, 99) < (int)(factor*100.0f)) pixels[i] = WHITE; else pixels[i] = BLACK; } Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } // Generate image: cellular algorithm. Bigger tileSize means bigger cells Image GenImageCellular(int width, int height, int tileSize) { Color *pixels = (Color *)RL_MALLOC(width*height*sizeof(Color)); int seedsPerRow = width/tileSize; int seedsPerCol = height/tileSize; int seedCount = seedsPerRow*seedsPerCol; Vector2 *seeds = (Vector2 *)RL_MALLOC(seedCount*sizeof(Vector2)); for (int i = 0; i < seedCount; i++) { int y = (i/seedsPerRow)*tileSize + GetRandomValue(0, tileSize - 1); int x = (i%seedsPerRow)*tileSize + GetRandomValue(0, tileSize - 1); seeds[i] = (Vector2){ (float)x, (float)y }; } for (int y = 0; y < height; y++) { int tileY = y/tileSize; for (int x = 0; x < width; x++) { int tileX = x/tileSize; float minDistance = 65536.0f; //(float)strtod("Inf", NULL); // Check all adjacent tiles for (int i = -1; i < 2; i++) { if ((tileX + i < 0) || (tileX + i >= seedsPerRow)) continue; for (int j = -1; j < 2; j++) { if ((tileY + j < 0) || (tileY + j >= seedsPerCol)) continue; Vector2 neighborSeed = seeds[(tileY + j)*seedsPerRow + tileX + i]; float dist = (float)hypot(x - (int)neighborSeed.x, y - (int)neighborSeed.y); minDistance = (float)fmin(minDistance, dist); } } // I made this up but it seems to give good results at all tile sizes int intensity = (int)(minDistance*256.0f/tileSize); if (intensity > 255) intensity = 255; pixels[y*width + x] = (Color){ intensity, intensity, intensity, 255 }; } } RL_FREE(seeds); Image image = { .data = pixels, .width = width, .height = height, .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, .mipmaps = 1 }; return image; } #endif // SUPPORT_IMAGE_GENERATION //------------------------------------------------------------------------------------ // Image manipulation functions //------------------------------------------------------------------------------------ // Copy an image to a new image Image ImageCopy(Image image) { Image newImage = { 0 }; int width = image.width; int height = image.height; int size = 0; for (int i = 0; i < image.mipmaps; i++) { size += GetPixelDataSize(width, height, image.format); width /= 2; height /= 2; // Security check for NPOT textures if (width < 1) width = 1; if (height < 1) height = 1; } newImage.data = RL_MALLOC(size); if (newImage.data != NULL) { // NOTE: Size must be provided in bytes memcpy(newImage.data, image.data, size); newImage.width = image.width; newImage.height = image.height; newImage.mipmaps = image.mipmaps; newImage.format = image.format; } return newImage; } // Create an image from another image piece Image ImageFromImage(Image image, Rectangle rec) { Image result = { 0 }; int bytesPerPixel = GetPixelDataSize(1, 1, image.format); result.width = (int)rec.width; result.height = (int)rec.height; result.data = RL_CALLOC((int)(rec.width*rec.height)*bytesPerPixel, 1); result.format = image.format; result.mipmaps = 1; for (int y = 0; y < rec.height; y++) { memcpy(((unsigned char *)result.data) + y*(int)rec.width*bytesPerPixel, ((unsigned char *)image.data) + ((y + (int)rec.y)*image.width + (int)rec.x)*bytesPerPixel, (int)rec.width*bytesPerPixel); } return result; } // Crop an image to area defined by a rectangle // NOTE: Security checks are performed in case rectangle goes out of bounds void ImageCrop(Image *image, Rectangle crop) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; // Security checks to validate crop rectangle if (crop.x < 0) { crop.width += crop.x; crop.x = 0; } if (crop.y < 0) { crop.height += crop.y; crop.y = 0; } if ((crop.x + crop.width) > image->width) crop.width = image->width - crop.x; if ((crop.y + crop.height) > image->height) crop.height = image->height - crop.y; if ((crop.x > image->width) || (crop.y > image->height)) { TRACELOG(LOG_WARNING, "IMAGE: Failed to crop, rectangle out of bounds"); return; } if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *croppedData = (unsigned char *)RL_MALLOC((int)(crop.width*crop.height)*bytesPerPixel); // OPTION 1: Move cropped data line-by-line for (int y = (int)crop.y, offsetSize = 0; y < (int)(crop.y + crop.height); y++) { memcpy(croppedData + offsetSize, ((unsigned char *)image->data) + (y*image->width + (int)crop.x)*bytesPerPixel, (int)crop.width*bytesPerPixel); offsetSize += ((int)crop.width*bytesPerPixel); } /* // OPTION 2: Move cropped data pixel-by-pixel or byte-by-byte for (int y = (int)crop.y; y < (int)(crop.y + crop.height); y++) { for (int x = (int)crop.x; x < (int)(crop.x + crop.width); x++) { //memcpy(croppedData + ((y - (int)crop.y)*(int)crop.width + (x - (int)crop.x))*bytesPerPixel, ((unsigned char *)image->data) + (y*image->width + x)*bytesPerPixel, bytesPerPixel); for (int i = 0; i < bytesPerPixel; i++) croppedData[((y - (int)crop.y)*(int)crop.width + (x - (int)crop.x))*bytesPerPixel + i] = ((unsigned char *)image->data)[(y*image->width + x)*bytesPerPixel + i]; } } */ RL_FREE(image->data); image->data = croppedData; image->width = (int)crop.width; image->height = (int)crop.height; } } // Convert image data to desired format void ImageFormat(Image *image, int newFormat) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if ((newFormat != 0) && (image->format != newFormat)) { if ((image->format < PIXELFORMAT_COMPRESSED_DXT1_RGB) && (newFormat < PIXELFORMAT_COMPRESSED_DXT1_RGB)) { Vector4 *pixels = LoadImageDataNormalized(*image); // Supports 8 to 32 bit per channel RL_FREE(image->data); // WARNING! We loose mipmaps data --> Regenerated at the end... image->data = NULL; image->format = newFormat; int k = 0; switch (image->format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { image->data = (unsigned char *)RL_MALLOC(image->width*image->height*sizeof(unsigned char)); for (int i = 0; i < image->width*image->height; i++) { ((unsigned char *)image->data)[i] = (unsigned char)((pixels[i].x*0.299f + pixels[i].y*0.587f + pixels[i].z*0.114f)*255.0f); } } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { image->data = (unsigned char *)RL_MALLOC(image->width*image->height*2*sizeof(unsigned char)); for (int i = 0; i < image->width*image->height*2; i += 2, k++) { ((unsigned char *)image->data)[i] = (unsigned char)((pixels[k].x*0.299f + (float)pixels[k].y*0.587f + (float)pixels[k].z*0.114f)*255.0f); ((unsigned char *)image->data)[i + 1] = (unsigned char)(pixels[k].w*255.0f); } } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { image->data = (unsigned short *)RL_MALLOC(image->width*image->height*sizeof(unsigned short)); unsigned char r = 0; unsigned char g = 0; unsigned char b = 0; for (int i = 0; i < image->width*image->height; i++) { r = (unsigned char)(round(pixels[i].x*31.0f)); g = (unsigned char)(round(pixels[i].y*63.0f)); b = (unsigned char)(round(pixels[i].z*31.0f)); ((unsigned short *)image->data)[i] = (unsigned short)r << 11 | (unsigned short)g << 5 | (unsigned short)b; } } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { image->data = (unsigned char *)RL_MALLOC(image->width*image->height*3*sizeof(unsigned char)); for (int i = 0, k = 0; i < image->width*image->height*3; i += 3, k++) { ((unsigned char *)image->data)[i] = (unsigned char)(pixels[k].x*255.0f); ((unsigned char *)image->data)[i + 1] = (unsigned char)(pixels[k].y*255.0f); ((unsigned char *)image->data)[i + 2] = (unsigned char)(pixels[k].z*255.0f); } } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { image->data = (unsigned short *)RL_MALLOC(image->width*image->height*sizeof(unsigned short)); unsigned char r = 0; unsigned char g = 0; unsigned char b = 0; unsigned char a = 0; for (int i = 0; i < image->width*image->height; i++) { r = (unsigned char)(round(pixels[i].x*31.0f)); g = (unsigned char)(round(pixels[i].y*31.0f)); b = (unsigned char)(round(pixels[i].z*31.0f)); a = (pixels[i].w > ((float)PIXELFORMAT_UNCOMPRESSED_R5G5B5A1_ALPHA_THRESHOLD/255.0f))? 1 : 0; ((unsigned short *)image->data)[i] = (unsigned short)r << 11 | (unsigned short)g << 6 | (unsigned short)b << 1 | (unsigned short)a; } } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { image->data = (unsigned short *)RL_MALLOC(image->width*image->height*sizeof(unsigned short)); unsigned char r = 0; unsigned char g = 0; unsigned char b = 0; unsigned char a = 0; for (int i = 0; i < image->width*image->height; i++) { r = (unsigned char)(round(pixels[i].x*15.0f)); g = (unsigned char)(round(pixels[i].y*15.0f)); b = (unsigned char)(round(pixels[i].z*15.0f)); a = (unsigned char)(round(pixels[i].w*15.0f)); ((unsigned short *)image->data)[i] = (unsigned short)r << 12 | (unsigned short)g << 8 | (unsigned short)b << 4 | (unsigned short)a; } } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { image->data = (unsigned char *)RL_MALLOC(image->width*image->height*4*sizeof(unsigned char)); for (int i = 0, k = 0; i < image->width*image->height*4; i += 4, k++) { ((unsigned char *)image->data)[i] = (unsigned char)(pixels[k].x*255.0f); ((unsigned char *)image->data)[i + 1] = (unsigned char)(pixels[k].y*255.0f); ((unsigned char *)image->data)[i + 2] = (unsigned char)(pixels[k].z*255.0f); ((unsigned char *)image->data)[i + 3] = (unsigned char)(pixels[k].w*255.0f); } } break; case PIXELFORMAT_UNCOMPRESSED_R32: { // WARNING: Image is converted to GRAYSCALE eqeuivalent 32bit image->data = (float *)RL_MALLOC(image->width*image->height*sizeof(float)); for (int i = 0; i < image->width*image->height; i++) { ((float *)image->data)[i] = (float)(pixels[i].x*0.299f + pixels[i].y*0.587f + pixels[i].z*0.114f); } } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { image->data = (float *)RL_MALLOC(image->width*image->height*3*sizeof(float)); for (int i = 0, k = 0; i < image->width*image->height*3; i += 3, k++) { ((float *)image->data)[i] = pixels[k].x; ((float *)image->data)[i + 1] = pixels[k].y; ((float *)image->data)[i + 2] = pixels[k].z; } } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { image->data = (float *)RL_MALLOC(image->width*image->height*4*sizeof(float)); for (int i = 0, k = 0; i < image->width*image->height*4; i += 4, k++) { ((float *)image->data)[i] = pixels[k].x; ((float *)image->data)[i + 1] = pixels[k].y; ((float *)image->data)[i + 2] = pixels[k].z; ((float *)image->data)[i + 3] = pixels[k].w; } } break; default: break; } RL_FREE(pixels); pixels = NULL; // In case original image had mipmaps, generate mipmaps for formated image // NOTE: Original mipmaps are replaced by new ones, if custom mipmaps were used, they are lost if (image->mipmaps > 1) { image->mipmaps = 1; #if defined(SUPPORT_IMAGE_MANIPULATION) if (image->data != NULL) ImageMipmaps(image); #endif } } else TRACELOG(LOG_WARNING, "IMAGE: Data format is compressed, can not be converted"); } } // Convert image to POT (power-of-two) // NOTE: It could be useful on OpenGL ES 2.0 (RPI, HTML5) void ImageToPOT(Image *image, Color fill) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; // Calculate next power-of-two values // NOTE: Just add the required amount of pixels at the right and bottom sides of image... int potWidth = (int)powf(2, ceilf(logf((float)image->width)/logf(2))); int potHeight = (int)powf(2, ceilf(logf((float)image->height)/logf(2))); // Check if POT texture generation is required (if texture is not already POT) if ((potWidth != image->width) || (potHeight != image->height)) ImageResizeCanvas(image, potWidth, potHeight, 0, 0, fill); } #if defined(SUPPORT_IMAGE_MANIPULATION) // Create an image from text (default font) Image ImageText(const char *text, int fontSize, Color color) { Image imText = { 0 }; #if defined(SUPPORT_MODULE_RTEXT) int defaultFontSize = 10; // Default Font chars height in pixel if (fontSize < defaultFontSize) fontSize = defaultFontSize; int spacing = fontSize/defaultFontSize; imText = ImageTextEx(GetFontDefault(), text, (float)fontSize, (float)spacing, color); // WARNING: Module required: rtext #else imText = GenImageColor(200, 60, BLACK); // Generating placeholder black image rectangle TRACELOG(LOG_WARNING, "IMAGE: ImageTextEx() requires module: rtext"); #endif return imText; } // Create an image from text (custom sprite font) Image ImageTextEx(Font font, const char *text, float fontSize, float spacing, Color tint) { Image imText = { 0 }; #if defined(SUPPORT_MODULE_RTEXT) int size = (int)strlen(text); // Get size in bytes of text int textOffsetX = 0; // Image drawing position X int textOffsetY = 0; // Offset between lines (on line break '\n') // NOTE: Text image is generated at font base size, later scaled to desired font size Vector2 imSize = MeasureTextEx(font, text, (float)font.baseSize, spacing); // WARNING: Module required: rtext // Create image to store text imText = GenImageColor((int)imSize.x, (int)imSize.y, BLANK); for (int i = 0; i < size; i++) { // Get next codepoint from byte string and glyph index in font int codepointByteCount = 0; int codepoint = GetCodepoint(&text[i], &codepointByteCount); // WARNING: Module required: rtext int index = GetGlyphIndex(font, codepoint); // WARNING: Module required: rtext // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) // but we need to draw all of the bad bytes using the '?' symbol moving one byte if (codepoint == 0x3f) codepointByteCount = 1; if (codepoint == '\n') { // NOTE: Fixed line spacing of 1.5 line-height // TODO: Support custom line spacing defined by user textOffsetY += (font.baseSize + font.baseSize/2); textOffsetX = 0; } else { if ((codepoint != ' ') && (codepoint != '\t')) { Rectangle rec = { (float)(textOffsetX + font.glyphs[index].offsetX), (float)(textOffsetY + font.glyphs[index].offsetY), (float)font.recs[index].width, (float)font.recs[index].height }; ImageDraw(&imText, font.glyphs[index].image, (Rectangle){ 0, 0, (float)font.glyphs[index].image.width, (float)font.glyphs[index].image.height }, rec, tint); } if (font.glyphs[index].advanceX == 0) textOffsetX += (int)(font.recs[index].width + spacing); else textOffsetX += font.glyphs[index].advanceX + (int)spacing; } i += (codepointByteCount - 1); // Move text bytes counter to next codepoint } // Scale image depending on text size if (fontSize > imSize.y) { float scaleFactor = fontSize/imSize.y; TRACELOG(LOG_INFO, "IMAGE: Text scaled by factor: %f", scaleFactor); // Using nearest-neighbor scaling algorithm for default font // WARNING: Module required: rtext if (font.texture.id == GetFontDefault().texture.id) ImageResizeNN(&imText, (int)(imSize.x*scaleFactor), (int)(imSize.y*scaleFactor)); else ImageResize(&imText, (int)(imSize.x*scaleFactor), (int)(imSize.y*scaleFactor)); } #else imText = GenImageColor(200, 60, BLACK); // Generating placeholder black image rectangle TRACELOG(LOG_WARNING, "IMAGE: ImageTextEx() requires module: rtext"); #endif return imText; } // Crop image depending on alpha value // NOTE: Threshold is defined as a percentatge: 0.0f -> 1.0f void ImageAlphaCrop(Image *image, float threshold) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; Rectangle crop = GetImageAlphaBorder(*image, threshold); // Crop if rectangle is valid if (((int)crop.width != 0) && ((int)crop.height != 0)) ImageCrop(image, crop); } // Clear alpha channel to desired color // NOTE: Threshold defines the alpha limit, 0.0f to 1.0f void ImageAlphaClear(Image *image, Color color, float threshold) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { switch (image->format) { case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { unsigned char thresholdValue = (unsigned char)(threshold*255.0f); for (int i = 1; i < image->width*image->height*2; i += 2) { if (((unsigned char *)image->data)[i] <= thresholdValue) { ((unsigned char *)image->data)[i - 1] = color.r; ((unsigned char *)image->data)[i] = color.a; } } } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { unsigned char thresholdValue = ((threshold < 0.5f)? 0 : 1); unsigned char r = (unsigned char)(round((float)color.r*31.0f)); unsigned char g = (unsigned char)(round((float)color.g*31.0f)); unsigned char b = (unsigned char)(round((float)color.b*31.0f)); unsigned char a = (color.a < 128)? 0 : 1; for (int i = 0; i < image->width*image->height; i++) { if ((((unsigned short *)image->data)[i] & 0b0000000000000001) <= thresholdValue) { ((unsigned short *)image->data)[i] = (unsigned short)r << 11 | (unsigned short)g << 6 | (unsigned short)b << 1 | (unsigned short)a; } } } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { unsigned char thresholdValue = (unsigned char)(threshold*15.0f); unsigned char r = (unsigned char)(round((float)color.r*15.0f)); unsigned char g = (unsigned char)(round((float)color.g*15.0f)); unsigned char b = (unsigned char)(round((float)color.b*15.0f)); unsigned char a = (unsigned char)(round((float)color.a*15.0f)); for (int i = 0; i < image->width*image->height; i++) { if ((((unsigned short *)image->data)[i] & 0x000f) <= thresholdValue) { ((unsigned short *)image->data)[i] = (unsigned short)r << 12 | (unsigned short)g << 8 | (unsigned short)b << 4 | (unsigned short)a; } } } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { unsigned char thresholdValue = (unsigned char)(threshold*255.0f); for (int i = 3; i < image->width*image->height*4; i += 4) { if (((unsigned char *)image->data)[i] <= thresholdValue) { ((unsigned char *)image->data)[i - 3] = color.r; ((unsigned char *)image->data)[i - 2] = color.g; ((unsigned char *)image->data)[i - 1] = color.b; ((unsigned char *)image->data)[i] = color.a; } } } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { for (int i = 3; i < image->width*image->height*4; i += 4) { if (((float *)image->data)[i] <= threshold) { ((float *)image->data)[i - 3] = (float)color.r/255.0f; ((float *)image->data)[i - 2] = (float)color.g/255.0f; ((float *)image->data)[i - 1] = (float)color.b/255.0f; ((float *)image->data)[i] = (float)color.a/255.0f; } } } break; default: break; } } } // Apply alpha mask to image // NOTE 1: Returned image is GRAY_ALPHA (16bit) or RGBA (32bit) // NOTE 2: alphaMask should be same size as image void ImageAlphaMask(Image *image, Image alphaMask) { if ((image->width != alphaMask.width) || (image->height != alphaMask.height)) { TRACELOG(LOG_WARNING, "IMAGE: Alpha mask must be same size as image"); } else if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) { TRACELOG(LOG_WARNING, "IMAGE: Alpha mask can not be applied to compressed data formats"); } else { // Force mask to be Grayscale Image mask = ImageCopy(alphaMask); if (mask.format != PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) ImageFormat(&mask, PIXELFORMAT_UNCOMPRESSED_GRAYSCALE); // In case image is only grayscale, we just add alpha channel if (image->format == PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) { unsigned char *data = (unsigned char *)RL_MALLOC(image->width*image->height*2); // Apply alpha mask to alpha channel for (int i = 0, k = 0; (i < mask.width*mask.height) || (i < image->width*image->height); i++, k += 2) { data[k] = ((unsigned char *)image->data)[i]; data[k + 1] = ((unsigned char *)mask.data)[i]; } RL_FREE(image->data); image->data = data; image->format = PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA; } else { // Convert image to RGBA if (image->format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) ImageFormat(image, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); // Apply alpha mask to alpha channel for (int i = 0, k = 3; (i < mask.width*mask.height) || (i < image->width*image->height); i++, k += 4) { ((unsigned char *)image->data)[k] = ((unsigned char *)mask.data)[i]; } } UnloadImage(mask); } } // Premultiply alpha channel void ImageAlphaPremultiply(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; float alpha = 0.0f; Color *pixels = LoadImageColors(*image); for (int i = 0; i < image->width*image->height; i++) { if (pixels[i].a == 0) { pixels[i].r = 0; pixels[i].g = 0; pixels[i].b = 0; } else if (pixels[i].a < 255) { alpha = (float)pixels[i].a/255.0f; pixels[i].r = (unsigned char)((float)pixels[i].r*alpha); pixels[i].g = (unsigned char)((float)pixels[i].g*alpha); pixels[i].b = (unsigned char)((float)pixels[i].b*alpha); } } RL_FREE(image->data); int format = image->format; image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } // Resize and image to new size // NOTE: Uses stb default scaling filters (both bicubic): // STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM // STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL (high-quality Catmull-Rom) void ImageResize(Image *image, int newWidth, int newHeight) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; bool fastPath = true; if ((image->format != PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) && (image->format != PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA) && (image->format != PIXELFORMAT_UNCOMPRESSED_R8G8B8) && (image->format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)) fastPath = true; if (fastPath) { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *output = (unsigned char *)RL_MALLOC(newWidth*newHeight*bytesPerPixel); switch (image->format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: stbir_resize_uint8((unsigned char *)image->data, image->width, image->height, 0, output, newWidth, newHeight, 0, 1); break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: stbir_resize_uint8((unsigned char *)image->data, image->width, image->height, 0, output, newWidth, newHeight, 0, 2); break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: stbir_resize_uint8((unsigned char *)image->data, image->width, image->height, 0, output, newWidth, newHeight, 0, 3); break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: stbir_resize_uint8((unsigned char *)image->data, image->width, image->height, 0, output, newWidth, newHeight, 0, 4); break; default: break; } RL_FREE(image->data); image->data = output; image->width = newWidth; image->height = newHeight; } else { // Get data as Color pixels array to work with it Color *pixels = LoadImageColors(*image); Color *output = (Color *)RL_MALLOC(newWidth*newHeight*sizeof(Color)); // NOTE: Color data is casted to (unsigned char *), there shouldn't been any problem... stbir_resize_uint8((unsigned char *)pixels, image->width, image->height, 0, (unsigned char *)output, newWidth, newHeight, 0, 4); int format = image->format; UnloadImageColors(pixels); RL_FREE(image->data); image->data = output; image->width = newWidth; image->height = newHeight; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); // Reformat 32bit RGBA image to original format } } // Resize and image to new size using Nearest-Neighbor scaling algorithm void ImageResizeNN(Image *image,int newWidth,int newHeight) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; Color *pixels = LoadImageColors(*image); Color *output = (Color *)RL_MALLOC(newWidth*newHeight*sizeof(Color)); // EDIT: added +1 to account for an early rounding problem int xRatio = (int)((image->width << 16)/newWidth) + 1; int yRatio = (int)((image->height << 16)/newHeight) + 1; int x2, y2; for (int y = 0; y < newHeight; y++) { for (int x = 0; x < newWidth; x++) { x2 = ((x*xRatio) >> 16); y2 = ((y*yRatio) >> 16); output[(y*newWidth) + x] = pixels[(y2*image->width) + x2] ; } } int format = image->format; RL_FREE(image->data); image->data = output; image->width = newWidth; image->height = newHeight; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); // Reformat 32bit RGBA image to original format UnloadImageColors(pixels); } // Resize canvas and fill with color // NOTE: Resize offset is relative to the top-left corner of the original image void ImageResizeCanvas(Image *image, int newWidth, int newHeight, int offsetX, int offsetY, Color fill) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else if ((newWidth != image->width) || (newHeight != image->height)) { Rectangle srcRec = { 0, 0, (float)image->width, (float)image->height }; Vector2 dstPos = { (float)offsetX, (float)offsetY }; if (offsetX < 0) { srcRec.x = (float)-offsetX; srcRec.width += (float)offsetX; dstPos.x = 0; } else if ((offsetX + image->width) > newWidth) srcRec.width = (float)(newWidth - offsetX); if (offsetY < 0) { srcRec.y = (float)-offsetY; srcRec.height += (float)offsetY; dstPos.y = 0; } else if ((offsetY + image->height) > newHeight) srcRec.height = (float)(newHeight - offsetY); if (newWidth < srcRec.width) srcRec.width = (float)newWidth; if (newHeight < srcRec.height) srcRec.height = (float)newHeight; int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *resizedData = (unsigned char *)RL_CALLOC(newWidth*newHeight*bytesPerPixel, 1); // TODO: Fill resized canvas with fill color (must be formatted to image->format) int dstOffsetSize = ((int)dstPos.y*newWidth + (int)dstPos.x)*bytesPerPixel; for (int y = 0; y < (int)srcRec.height; y++) { memcpy(resizedData + dstOffsetSize, ((unsigned char *)image->data) + ((y + (int)srcRec.y)*image->width + (int)srcRec.x)*bytesPerPixel, (int)srcRec.width*bytesPerPixel); dstOffsetSize += (newWidth*bytesPerPixel); } RL_FREE(image->data); image->data = resizedData; image->width = newWidth; image->height = newHeight; } } // Generate all mipmap levels for a provided image // NOTE 1: Supports POT and NPOT images // NOTE 2: image.data is scaled to include mipmap levels // NOTE 3: Mipmaps format is the same as base image void ImageMipmaps(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; int mipCount = 1; // Required mipmap levels count (including base level) int mipWidth = image->width; // Base image width int mipHeight = image->height; // Base image height int mipSize = GetPixelDataSize(mipWidth, mipHeight, image->format); // Image data size (in bytes) // Count mipmap levels required while ((mipWidth != 1) || (mipHeight != 1)) { if (mipWidth != 1) mipWidth /= 2; if (mipHeight != 1) mipHeight /= 2; // Security check for NPOT textures if (mipWidth < 1) mipWidth = 1; if (mipHeight < 1) mipHeight = 1; TRACELOGD("IMAGE: Next mipmap level: %i x %i - current size %i", mipWidth, mipHeight, mipSize); mipCount++; mipSize += GetPixelDataSize(mipWidth, mipHeight, image->format); // Add mipmap size (in bytes) } if (image->mipmaps < mipCount) { void *temp = RL_REALLOC(image->data, mipSize); if (temp != NULL) image->data = temp; // Assign new pointer (new size) to store mipmaps data else TRACELOG(LOG_WARNING, "IMAGE: Mipmaps required memory could not be allocated"); // Pointer to allocated memory point where store next mipmap level data unsigned char *nextmip = (unsigned char *)image->data + GetPixelDataSize(image->width, image->height, image->format); mipWidth = image->width/2; mipHeight = image->height/2; mipSize = GetPixelDataSize(mipWidth, mipHeight, image->format); Image imCopy = ImageCopy(*image); for (int i = 1; i < mipCount; i++) { TRACELOGD("IMAGE: Generating mipmap level: %i (%i x %i) - size: %i - offset: 0x%x", i, mipWidth, mipHeight, mipSize, nextmip); ImageResize(&imCopy, mipWidth, mipHeight); // Uses internally Mitchell cubic downscale filter memcpy(nextmip, imCopy.data, mipSize); nextmip += mipSize; image->mipmaps++; mipWidth /= 2; mipHeight /= 2; // Security check for NPOT textures if (mipWidth < 1) mipWidth = 1; if (mipHeight < 1) mipHeight = 1; mipSize = GetPixelDataSize(mipWidth, mipHeight, image->format); } UnloadImage(imCopy); } else TRACELOG(LOG_WARNING, "IMAGE: Mipmaps already available"); } // Dither image data to 16bpp or lower (Floyd-Steinberg dithering) // NOTE: In case selected bpp do not represent an known 16bit format, // dithered data is stored in the LSB part of the unsigned short void ImageDither(Image *image, int rBpp, int gBpp, int bBpp, int aBpp) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) { TRACELOG(LOG_WARNING, "IMAGE: Compressed data formats can not be dithered"); return; } if ((rBpp + gBpp + bBpp + aBpp) > 16) { TRACELOG(LOG_WARNING, "IMAGE: Unsupported dithering bpps (%ibpp), only 16bpp or lower modes supported", (rBpp+gBpp+bBpp+aBpp)); } else { Color *pixels = LoadImageColors(*image); RL_FREE(image->data); // free old image data if ((image->format != PIXELFORMAT_UNCOMPRESSED_R8G8B8) && (image->format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)) { TRACELOG(LOG_WARNING, "IMAGE: Format is already 16bpp or lower, dithering could have no effect"); } // Define new image format, check if desired bpp match internal known format if ((rBpp == 5) && (gBpp == 6) && (bBpp == 5) && (aBpp == 0)) image->format = PIXELFORMAT_UNCOMPRESSED_R5G6B5; else if ((rBpp == 5) && (gBpp == 5) && (bBpp == 5) && (aBpp == 1)) image->format = PIXELFORMAT_UNCOMPRESSED_R5G5B5A1; else if ((rBpp == 4) && (gBpp == 4) && (bBpp == 4) && (aBpp == 4)) image->format = PIXELFORMAT_UNCOMPRESSED_R4G4B4A4; else { image->format = 0; TRACELOG(LOG_WARNING, "IMAGE: Unsupported dithered OpenGL internal format: %ibpp (R%iG%iB%iA%i)", (rBpp+gBpp+bBpp+aBpp), rBpp, gBpp, bBpp, aBpp); } // NOTE: We will store the dithered data as unsigned short (16bpp) image->data = (unsigned short *)RL_MALLOC(image->width*image->height*sizeof(unsigned short)); Color oldPixel = WHITE; Color newPixel = WHITE; int rError, gError, bError; unsigned short rPixel, gPixel, bPixel, aPixel; // Used for 16bit pixel composition #define MIN(a,b) (((a)<(b))?(a):(b)) for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { oldPixel = pixels[y*image->width + x]; // NOTE: New pixel obtained by bits truncate, it would be better to round values (check ImageFormat()) newPixel.r = oldPixel.r >> (8 - rBpp); // R bits newPixel.g = oldPixel.g >> (8 - gBpp); // G bits newPixel.b = oldPixel.b >> (8 - bBpp); // B bits newPixel.a = oldPixel.a >> (8 - aBpp); // A bits (not used on dithering) // NOTE: Error must be computed between new and old pixel but using same number of bits! // We want to know how much color precision we have lost... rError = (int)oldPixel.r - (int)(newPixel.r << (8 - rBpp)); gError = (int)oldPixel.g - (int)(newPixel.g << (8 - gBpp)); bError = (int)oldPixel.b - (int)(newPixel.b << (8 - bBpp)); pixels[y*image->width + x] = newPixel; // NOTE: Some cases are out of the array and should be ignored if (x < (image->width - 1)) { pixels[y*image->width + x+1].r = MIN((int)pixels[y*image->width + x+1].r + (int)((float)rError*7.0f/16), 0xff); pixels[y*image->width + x+1].g = MIN((int)pixels[y*image->width + x+1].g + (int)((float)gError*7.0f/16), 0xff); pixels[y*image->width + x+1].b = MIN((int)pixels[y*image->width + x+1].b + (int)((float)bError*7.0f/16), 0xff); } if ((x > 0) && (y < (image->height - 1))) { pixels[(y+1)*image->width + x-1].r = MIN((int)pixels[(y+1)*image->width + x-1].r + (int)((float)rError*3.0f/16), 0xff); pixels[(y+1)*image->width + x-1].g = MIN((int)pixels[(y+1)*image->width + x-1].g + (int)((float)gError*3.0f/16), 0xff); pixels[(y+1)*image->width + x-1].b = MIN((int)pixels[(y+1)*image->width + x-1].b + (int)((float)bError*3.0f/16), 0xff); } if (y < (image->height - 1)) { pixels[(y+1)*image->width + x].r = MIN((int)pixels[(y+1)*image->width + x].r + (int)((float)rError*5.0f/16), 0xff); pixels[(y+1)*image->width + x].g = MIN((int)pixels[(y+1)*image->width + x].g + (int)((float)gError*5.0f/16), 0xff); pixels[(y+1)*image->width + x].b = MIN((int)pixels[(y+1)*image->width + x].b + (int)((float)bError*5.0f/16), 0xff); } if ((x < (image->width - 1)) && (y < (image->height - 1))) { pixels[(y+1)*image->width + x+1].r = MIN((int)pixels[(y+1)*image->width + x+1].r + (int)((float)rError*1.0f/16), 0xff); pixels[(y+1)*image->width + x+1].g = MIN((int)pixels[(y+1)*image->width + x+1].g + (int)((float)gError*1.0f/16), 0xff); pixels[(y+1)*image->width + x+1].b = MIN((int)pixels[(y+1)*image->width + x+1].b + (int)((float)bError*1.0f/16), 0xff); } rPixel = (unsigned short)newPixel.r; gPixel = (unsigned short)newPixel.g; bPixel = (unsigned short)newPixel.b; aPixel = (unsigned short)newPixel.a; ((unsigned short *)image->data)[y*image->width + x] = (rPixel << (gBpp + bBpp + aBpp)) | (gPixel << (bBpp + aBpp)) | (bPixel << aBpp) | aPixel; } } UnloadImageColors(pixels); } } // Flip image vertically void ImageFlipVertical(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *flippedData = (unsigned char *)RL_MALLOC(image->width*image->height*bytesPerPixel); for (int i = (image->height - 1), offsetSize = 0; i >= 0; i--) { memcpy(flippedData + offsetSize, ((unsigned char *)image->data) + i*image->width*bytesPerPixel, image->width*bytesPerPixel); offsetSize += image->width*bytesPerPixel; } RL_FREE(image->data); image->data = flippedData; } } // Flip image horizontally void ImageFlipHorizontal(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *flippedData = (unsigned char *)RL_MALLOC(image->width*image->height*bytesPerPixel); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { // OPTION 1: Move pixels with memcopy() //memcpy(flippedData + (y*image->width + x)*bytesPerPixel, ((unsigned char *)image->data) + (y*image->width + (image->width - 1 - x))*bytesPerPixel, bytesPerPixel); // OPTION 2: Just copy data pixel by pixel for (int i = 0; i < bytesPerPixel; i++) flippedData[(y*image->width + x)*bytesPerPixel + i] = ((unsigned char *)image->data)[(y*image->width + (image->width - 1 - x))*bytesPerPixel + i]; } } RL_FREE(image->data); image->data = flippedData; /* // OPTION 3: Faster implementation (specific for 32bit pixels) // NOTE: It does not require additional allocations uint32_t *ptr = (uint32_t *)image->data; for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width/2; x++) { uint32_t backup = ptr[y*image->width + x]; ptr[y*image->width + x] = ptr[y*image->width + (image->width - 1 - x)]; ptr[y*image->width + (image->width - 1 - x)] = backup; } } */ } } // Rotate image clockwise 90deg void ImageRotateCW(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *rotatedData = (unsigned char *)RL_MALLOC(image->width*image->height*bytesPerPixel); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { //memcpy(rotatedData + (x*image->height + (image->height - y - 1))*bytesPerPixel, ((unsigned char *)image->data) + (y*image->width + x)*bytesPerPixel, bytesPerPixel); for (int i = 0; i < bytesPerPixel; i++) rotatedData[(x*image->height + (image->height - y - 1))*bytesPerPixel + i] = ((unsigned char *)image->data)[(y*image->width + x)*bytesPerPixel + i]; } } RL_FREE(image->data); image->data = rotatedData; int width = image->width; int height = image-> height; image->width = height; image->height = width; } } // Rotate image counter-clockwise 90deg void ImageRotateCCW(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (image->mipmaps > 1) TRACELOG(LOG_WARNING, "Image manipulation only applied to base mipmap level"); if (image->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image manipulation not supported for compressed formats"); else { int bytesPerPixel = GetPixelDataSize(1, 1, image->format); unsigned char *rotatedData = (unsigned char *)RL_MALLOC(image->width*image->height*bytesPerPixel); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { //memcpy(rotatedData + (x*image->height + y))*bytesPerPixel, ((unsigned char *)image->data) + (y*image->width + (image->width - x - 1))*bytesPerPixel, bytesPerPixel); for (int i = 0; i < bytesPerPixel; i++) rotatedData[(x*image->height + y)*bytesPerPixel + i] = ((unsigned char *)image->data)[(y*image->width + (image->width - x - 1))*bytesPerPixel + i]; } } RL_FREE(image->data); image->data = rotatedData; int width = image->width; int height = image-> height; image->width = height; image->height = width; } } // Modify image color: tint void ImageColorTint(Image *image, Color color) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; Color *pixels = LoadImageColors(*image); float cR = (float)color.r/255; float cG = (float)color.g/255; float cB = (float)color.b/255; float cA = (float)color.a/255; for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { int index = y*image->width + x; unsigned char r = (unsigned char)(((float)pixels[index].r/255*cR)*255.0f); unsigned char g = (unsigned char)(((float)pixels[index].g/255*cG)*255.0f); unsigned char b = (unsigned char)(((float)pixels[index].b/255*cB)*255.0f); unsigned char a = (unsigned char)(((float)pixels[index].a/255*cA)*255.0f); pixels[y*image->width + x].r = r; pixels[y*image->width + x].g = g; pixels[y*image->width + x].b = b; pixels[y*image->width + x].a = a; } } int format = image->format; RL_FREE(image->data); image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } // Modify image color: invert void ImageColorInvert(Image *image) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; Color *pixels = LoadImageColors(*image); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { pixels[y*image->width + x].r = 255 - pixels[y*image->width + x].r; pixels[y*image->width + x].g = 255 - pixels[y*image->width + x].g; pixels[y*image->width + x].b = 255 - pixels[y*image->width + x].b; } } int format = image->format; RL_FREE(image->data); image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } // Modify image color: grayscale void ImageColorGrayscale(Image *image) { ImageFormat(image, PIXELFORMAT_UNCOMPRESSED_GRAYSCALE); } // Modify image color: contrast // NOTE: Contrast values between -100 and 100 void ImageColorContrast(Image *image, float contrast) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (contrast < -100) contrast = -100; if (contrast > 100) contrast = 100; contrast = (100.0f + contrast)/100.0f; contrast *= contrast; Color *pixels = LoadImageColors(*image); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { float pR = (float)pixels[y*image->width + x].r/255.0f; pR -= 0.5; pR *= contrast; pR += 0.5; pR *= 255; if (pR < 0) pR = 0; if (pR > 255) pR = 255; float pG = (float)pixels[y*image->width + x].g/255.0f; pG -= 0.5; pG *= contrast; pG += 0.5; pG *= 255; if (pG < 0) pG = 0; if (pG > 255) pG = 255; float pB = (float)pixels[y*image->width + x].b/255.0f; pB -= 0.5; pB *= contrast; pB += 0.5; pB *= 255; if (pB < 0) pB = 0; if (pB > 255) pB = 255; pixels[y*image->width + x].r = (unsigned char)pR; pixels[y*image->width + x].g = (unsigned char)pG; pixels[y*image->width + x].b = (unsigned char)pB; } } int format = image->format; RL_FREE(image->data); image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } // Modify image color: brightness // NOTE: Brightness values between -255 and 255 void ImageColorBrightness(Image *image, int brightness) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; if (brightness < -255) brightness = -255; if (brightness > 255) brightness = 255; Color *pixels = LoadImageColors(*image); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { int cR = pixels[y*image->width + x].r + brightness; int cG = pixels[y*image->width + x].g + brightness; int cB = pixels[y*image->width + x].b + brightness; if (cR < 0) cR = 1; if (cR > 255) cR = 255; if (cG < 0) cG = 1; if (cG > 255) cG = 255; if (cB < 0) cB = 1; if (cB > 255) cB = 255; pixels[y*image->width + x].r = (unsigned char)cR; pixels[y*image->width + x].g = (unsigned char)cG; pixels[y*image->width + x].b = (unsigned char)cB; } } int format = image->format; RL_FREE(image->data); image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } // Modify image color: replace color void ImageColorReplace(Image *image, Color color, Color replace) { // Security check to avoid program crash if ((image->data == NULL) || (image->width == 0) || (image->height == 0)) return; Color *pixels = LoadImageColors(*image); for (int y = 0; y < image->height; y++) { for (int x = 0; x < image->width; x++) { if ((pixels[y*image->width + x].r == color.r) && (pixels[y*image->width + x].g == color.g) && (pixels[y*image->width + x].b == color.b) && (pixels[y*image->width + x].a == color.a)) { pixels[y*image->width + x].r = replace.r; pixels[y*image->width + x].g = replace.g; pixels[y*image->width + x].b = replace.b; pixels[y*image->width + x].a = replace.a; } } } int format = image->format; RL_FREE(image->data); image->data = pixels; image->format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; ImageFormat(image, format); } #endif // SUPPORT_IMAGE_MANIPULATION // Load color data from image as a Color array (RGBA - 32bit) // NOTE: Memory allocated should be freed using UnloadImageColors(); Color *LoadImageColors(Image image) { if ((image.width == 0) || (image.height == 0)) return NULL; Color *pixels = (Color *)RL_MALLOC(image.width*image.height*sizeof(Color)); if (image.format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "IMAGE: Pixel data retrieval not supported for compressed image formats"); else { if ((image.format == PIXELFORMAT_UNCOMPRESSED_R32) || (image.format == PIXELFORMAT_UNCOMPRESSED_R32G32B32) || (image.format == PIXELFORMAT_UNCOMPRESSED_R32G32B32A32)) TRACELOG(LOG_WARNING, "IMAGE: Pixel format converted from 32bit to 8bit per channel"); for (int i = 0, k = 0; i < image.width*image.height; i++) { switch (image.format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { pixels[i].r = ((unsigned char *)image.data)[i]; pixels[i].g = ((unsigned char *)image.data)[i]; pixels[i].b = ((unsigned char *)image.data)[i]; pixels[i].a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { pixels[i].r = ((unsigned char *)image.data)[k]; pixels[i].g = ((unsigned char *)image.data)[k]; pixels[i].b = ((unsigned char *)image.data)[k]; pixels[i].a = ((unsigned char *)image.data)[k + 1]; k += 2; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].r = (unsigned char)((float)((pixel & 0b1111100000000000) >> 11)*(255/31)); pixels[i].g = (unsigned char)((float)((pixel & 0b0000011111000000) >> 6)*(255/31)); pixels[i].b = (unsigned char)((float)((pixel & 0b0000000000111110) >> 1)*(255/31)); pixels[i].a = (unsigned char)((pixel & 0b0000000000000001)*255); } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].r = (unsigned char)((float)((pixel & 0b1111100000000000) >> 11)*(255/31)); pixels[i].g = (unsigned char)((float)((pixel & 0b0000011111100000) >> 5)*(255/63)); pixels[i].b = (unsigned char)((float)(pixel & 0b0000000000011111)*(255/31)); pixels[i].a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].r = (unsigned char)((float)((pixel & 0b1111000000000000) >> 12)*(255/15)); pixels[i].g = (unsigned char)((float)((pixel & 0b0000111100000000) >> 8)*(255/15)); pixels[i].b = (unsigned char)((float)((pixel & 0b0000000011110000) >> 4)*(255/15)); pixels[i].a = (unsigned char)((float)(pixel & 0b0000000000001111)*(255/15)); } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { pixels[i].r = ((unsigned char *)image.data)[k]; pixels[i].g = ((unsigned char *)image.data)[k + 1]; pixels[i].b = ((unsigned char *)image.data)[k + 2]; pixels[i].a = ((unsigned char *)image.data)[k + 3]; k += 4; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { pixels[i].r = (unsigned char)((unsigned char *)image.data)[k]; pixels[i].g = (unsigned char)((unsigned char *)image.data)[k + 1]; pixels[i].b = (unsigned char)((unsigned char *)image.data)[k + 2]; pixels[i].a = 255; k += 3; } break; case PIXELFORMAT_UNCOMPRESSED_R32: { pixels[i].r = (unsigned char)(((float *)image.data)[k]*255.0f); pixels[i].g = 0; pixels[i].b = 0; pixels[i].a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { pixels[i].r = (unsigned char)(((float *)image.data)[k]*255.0f); pixels[i].g = (unsigned char)(((float *)image.data)[k + 1]*255.0f); pixels[i].b = (unsigned char)(((float *)image.data)[k + 2]*255.0f); pixels[i].a = 255; k += 3; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { pixels[i].r = (unsigned char)(((float *)image.data)[k]*255.0f); pixels[i].g = (unsigned char)(((float *)image.data)[k]*255.0f); pixels[i].b = (unsigned char)(((float *)image.data)[k]*255.0f); pixels[i].a = (unsigned char)(((float *)image.data)[k]*255.0f); k += 4; } break; default: break; } } } return pixels; } // Load colors palette from image as a Color array (RGBA - 32bit) // NOTE: Memory allocated should be freed using UnloadImagePalette() Color *LoadImagePalette(Image image, int maxPaletteSize, int *colorCount) { #define COLOR_EQUAL(col1, col2) ((col1.r == col2.r)&&(col1.g == col2.g)&&(col1.b == col2.b)&&(col1.a == col2.a)) int palCount = 0; Color *palette = NULL; Color *pixels = LoadImageColors(image); if (pixels != NULL) { palette = (Color *)RL_MALLOC(maxPaletteSize*sizeof(Color)); for (int i = 0; i < maxPaletteSize; i++) palette[i] = BLANK; // Set all colors to BLANK for (int i = 0; i < image.width*image.height; i++) { if (pixels[i].a > 0) { bool colorInPalette = false; // Check if the color is already on palette for (int j = 0; j < maxPaletteSize; j++) { if (COLOR_EQUAL(pixels[i], palette[j])) { colorInPalette = true; break; } } // Store color if not on the palette if (!colorInPalette) { palette[palCount] = pixels[i]; // Add pixels[i] to palette palCount++; // We reached the limit of colors supported by palette if (palCount >= maxPaletteSize) { i = image.width*image.height; // Finish palette get TRACELOG(LOG_WARNING, "IMAGE: Palette is greater than %i colors", maxPaletteSize); } } } } UnloadImageColors(pixels); } *colorCount = palCount; return palette; } // Unload color data loaded with LoadImageColors() void UnloadImageColors(Color *colors) { RL_FREE(colors); } // Unload colors palette loaded with LoadImagePalette() void UnloadImagePalette(Color *colors) { RL_FREE(colors); } // Get image alpha border rectangle // NOTE: Threshold is defined as a percentatge: 0.0f -> 1.0f Rectangle GetImageAlphaBorder(Image image, float threshold) { Rectangle crop = { 0 }; Color *pixels = LoadImageColors(image); if (pixels != NULL) { int xMin = 65536; // Define a big enough number int xMax = 0; int yMin = 65536; int yMax = 0; for (int y = 0; y < image.height; y++) { for (int x = 0; x < image.width; x++) { if (pixels[y*image.width + x].a > (unsigned char)(threshold*255.0f)) { if (x < xMin) xMin = x; if (x > xMax) xMax = x; if (y < yMin) yMin = y; if (y > yMax) yMax = y; } } } // Check for empty blank image if ((xMin != 65536) && (xMax != 65536)) { crop = (Rectangle){ (float)xMin, (float)yMin, (float)((xMax + 1) - xMin), (float)((yMax + 1) - yMin) }; } UnloadImageColors(pixels); } return crop; } // Get image pixel color at (x, y) position Color GetImageColor(Image image, int x, int y) { Color color = { 0 }; if ((x >=0) && (x < image.width) && (y >= 0) && (y < image.height)) { switch (image.format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { color.r = ((unsigned char *)image.data)[y*image.width + x]; color.g = ((unsigned char *)image.data)[y*image.width + x]; color.b = ((unsigned char *)image.data)[y*image.width + x]; color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { color.r = ((unsigned char *)image.data)[(y*image.width + x)*2]; color.g = ((unsigned char *)image.data)[(y*image.width + x)*2]; color.b = ((unsigned char *)image.data)[(y*image.width + x)*2]; color.a = ((unsigned char *)image.data)[(y*image.width + x)*2 + 1]; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { unsigned short pixel = ((unsigned short *)image.data)[y*image.width + x]; color.r = (unsigned char)((float)((pixel & 0b1111100000000000) >> 11)*(255/31)); color.g = (unsigned char)((float)((pixel & 0b0000011111000000) >> 6)*(255/31)); color.b = (unsigned char)((float)((pixel & 0b0000000000111110) >> 1)*(255/31)); color.a = (unsigned char)((pixel & 0b0000000000000001)*255); } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { unsigned short pixel = ((unsigned short *)image.data)[y*image.width + x]; color.r = (unsigned char)((float)((pixel & 0b1111100000000000) >> 11)*(255/31)); color.g = (unsigned char)((float)((pixel & 0b0000011111100000) >> 5)*(255/63)); color.b = (unsigned char)((float)(pixel & 0b0000000000011111)*(255/31)); color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { unsigned short pixel = ((unsigned short *)image.data)[y*image.width + x]; color.r = (unsigned char)((float)((pixel & 0b1111000000000000) >> 12)*(255/15)); color.g = (unsigned char)((float)((pixel & 0b0000111100000000) >> 8)*(255/15)); color.b = (unsigned char)((float)((pixel & 0b0000000011110000) >> 4)*(255/15)); color.a = (unsigned char)((float)(pixel & 0b0000000000001111)*(255/15)); } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { color.r = ((unsigned char *)image.data)[(y*image.width + x)*4]; color.g = ((unsigned char *)image.data)[(y*image.width + x)*4 + 1]; color.b = ((unsigned char *)image.data)[(y*image.width + x)*4 + 2]; color.a = ((unsigned char *)image.data)[(y*image.width + x)*4 + 3]; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { color.r = (unsigned char)((unsigned char *)image.data)[(y*image.width + x)*3]; color.g = (unsigned char)((unsigned char *)image.data)[(y*image.width + x)*3 + 1]; color.b = (unsigned char)((unsigned char *)image.data)[(y*image.width + x)*3 + 2]; color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32: { color.r = (unsigned char)(((float *)image.data)[y*image.width + x]*255.0f); color.g = 0; color.b = 0; color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { color.r = (unsigned char)(((float *)image.data)[(y*image.width + x)*3]*255.0f); color.g = (unsigned char)(((float *)image.data)[(y*image.width + x)*3 + 1]*255.0f); color.b = (unsigned char)(((float *)image.data)[(y*image.width + x)*3 + 2]*255.0f); color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { color.r = (unsigned char)(((float *)image.data)[(y*image.width + x)*4]*255.0f); color.g = (unsigned char)(((float *)image.data)[(y*image.width + x)*4]*255.0f); color.b = (unsigned char)(((float *)image.data)[(y*image.width + x)*4]*255.0f); color.a = (unsigned char)(((float *)image.data)[(y*image.width + x)*4]*255.0f); } break; default: TRACELOG(LOG_WARNING, "Compressed image format does not support color reading"); break; } } else TRACELOG(LOG_WARNING, "Requested image pixel (%i, %i) out of bounds", x, y); return color; } //------------------------------------------------------------------------------------ // Image drawing functions //------------------------------------------------------------------------------------ // Clear image background with given color void ImageClearBackground(Image *dst, Color color) { for (int i = 0; i < dst->width*dst->height; ++i) ImageDrawPixel(dst, i%dst->width, i/dst->width, color); } // Draw pixel within an image // NOTE: Compressed image formats not supported void ImageDrawPixel(Image *dst, int x, int y, Color color) { // Security check to avoid program crash if ((dst->data == NULL) || (x < 0) || (x >= dst->width) || (y < 0) || (y >= dst->height)) return; switch (dst->format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { // NOTE: Calculate grayscale equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char gray = (unsigned char)((coln.x*0.299f + coln.y*0.587f + coln.z*0.114f)*255.0f); ((unsigned char *)dst->data)[y*dst->width + x] = gray; } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { // NOTE: Calculate grayscale equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char gray = (unsigned char)((coln.x*0.299f + coln.y*0.587f + coln.z*0.114f)*255.0f); ((unsigned char *)dst->data)[(y*dst->width + x)*2] = gray; ((unsigned char *)dst->data)[(y*dst->width + x)*2 + 1] = color.a; } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { // NOTE: Calculate R5G6B5 equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char r = (unsigned char)(round(coln.x*31.0f)); unsigned char g = (unsigned char)(round(coln.y*63.0f)); unsigned char b = (unsigned char)(round(coln.z*31.0f)); ((unsigned short *)dst->data)[y*dst->width + x] = (unsigned short)r << 11 | (unsigned short)g << 5 | (unsigned short)b; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { // NOTE: Calculate R5G5B5A1 equivalent color Vector4 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f, (float)color.a/255.0f }; unsigned char r = (unsigned char)(round(coln.x*31.0f)); unsigned char g = (unsigned char)(round(coln.y*31.0f)); unsigned char b = (unsigned char)(round(coln.z*31.0f)); unsigned char a = (coln.w > ((float)PIXELFORMAT_UNCOMPRESSED_R5G5B5A1_ALPHA_THRESHOLD/255.0f))? 1 : 0; ((unsigned short *)dst->data)[y*dst->width + x] = (unsigned short)r << 11 | (unsigned short)g << 6 | (unsigned short)b << 1 | (unsigned short)a; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { // NOTE: Calculate R5G5B5A1 equivalent color Vector4 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f, (float)color.a/255.0f }; unsigned char r = (unsigned char)(round(coln.x*15.0f)); unsigned char g = (unsigned char)(round(coln.y*15.0f)); unsigned char b = (unsigned char)(round(coln.z*15.0f)); unsigned char a = (unsigned char)(round(coln.w*15.0f)); ((unsigned short *)dst->data)[y*dst->width + x] = (unsigned short)r << 12 | (unsigned short)g << 8 | (unsigned short)b << 4 | (unsigned short)a; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { ((unsigned char *)dst->data)[(y*dst->width + x)*3] = color.r; ((unsigned char *)dst->data)[(y*dst->width + x)*3 + 1] = color.g; ((unsigned char *)dst->data)[(y*dst->width + x)*3 + 2] = color.b; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { ((unsigned char *)dst->data)[(y*dst->width + x)*4] = color.r; ((unsigned char *)dst->data)[(y*dst->width + x)*4 + 1] = color.g; ((unsigned char *)dst->data)[(y*dst->width + x)*4 + 2] = color.b; ((unsigned char *)dst->data)[(y*dst->width + x)*4 + 3] = color.a; } break; case PIXELFORMAT_UNCOMPRESSED_R32: { // NOTE: Calculate grayscale equivalent color (normalized to 32bit) Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; ((float *)dst->data)[y*dst->width + x] = coln.x*0.299f + coln.y*0.587f + coln.z*0.114f; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { // NOTE: Calculate R32G32B32 equivalent color (normalized to 32bit) Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; ((float *)dst->data)[(y*dst->width + x)*3] = coln.x; ((float *)dst->data)[(y*dst->width + x)*3 + 1] = coln.y; ((float *)dst->data)[(y*dst->width + x)*3 + 2] = coln.z; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { // NOTE: Calculate R32G32B32A32 equivalent color (normalized to 32bit) Vector4 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f, (float)color.a/255.0f }; ((float *)dst->data)[(y*dst->width + x)*4] = coln.x; ((float *)dst->data)[(y*dst->width + x)*4 + 1] = coln.y; ((float *)dst->data)[(y*dst->width + x)*4 + 2] = coln.z; ((float *)dst->data)[(y*dst->width + x)*4 + 3] = coln.w; } break; default: break; } } // Draw pixel within an image (Vector version) void ImageDrawPixelV(Image *dst, Vector2 position, Color color) { ImageDrawPixel(dst, (int)position.x, (int)position.y, color); } // Draw line within an image void ImageDrawLine(Image *dst, int startPosX, int startPosY, int endPosX, int endPosY, Color color) { // Using Bresenham's algorithm as described in // Drawing Lines with Pixels - Joshua Scott - March 2012 // https://classic.csunplugged.org/wp-content/uploads/2014/12/Lines.pdf int changeInX = (endPosX - startPosX); int absChangeInX = (changeInX < 0)? -changeInX : changeInX; int changeInY = (endPosY - startPosY); int absChangeInY = (changeInY < 0)? -changeInY : changeInY; int startU, startV, endU, stepV; // Substitutions, either U = X, V = Y or vice versa. See loop at end of function //int endV; // Not needed but left for better understanding, check code below int A, B, P; // See linked paper above, explained down in the main loop int reversedXY = (absChangeInY < absChangeInX); if (reversedXY) { A = 2*absChangeInY; B = A - 2*absChangeInX; P = A - absChangeInX; if (changeInX > 0) { startU = startPosX; startV = startPosY; endU = endPosX; //endV = endPosY; } else { startU = endPosX; startV = endPosY; endU = startPosX; //endV = startPosY; // Since start and end are reversed changeInX = -changeInX; changeInY = -changeInY; } stepV = (changeInY < 0)? -1 : 1; ImageDrawPixel(dst, startU, startV, color); // At this point they are correctly ordered... } else { A = 2*absChangeInX; B = A - 2*absChangeInY; P = A - absChangeInY; if (changeInY > 0) { startU = startPosY; startV = startPosX; endU = endPosY; //endV = endPosX; } else { startU = endPosY; startV = endPosX; endU = startPosY; //endV = startPosX; // Since start and end are reversed changeInX = -changeInX; changeInY = -changeInY; } stepV = (changeInX < 0)? -1 : 1; ImageDrawPixel(dst, startV, startU, color); // ... but need to be reversed here. Repeated in the main loop below } // We already drew the start point. If we started at startU + 0, the line would be crooked and too short for (int u = startU + 1, v = startV; u <= endU; u++) { if (P >= 0) { v += stepV; // Adjusts whenever we stray too far from the direct line. Details in the linked paper above P += B; // Remembers that we corrected our path } else P += A; // Remembers how far we are from the direct line if (reversedXY) ImageDrawPixel(dst, u, v, color); else ImageDrawPixel(dst, v, u, color); } } // Draw line within an image (Vector version) void ImageDrawLineV(Image *dst, Vector2 start, Vector2 end, Color color) { ImageDrawLine(dst, (int)start.x, (int)start.y, (int)end.x, (int)end.y, color); } // Draw circle within an image void ImageDrawCircle(Image *dst, int centerX, int centerY, int radius, Color color) { int x = 0, y = radius; int decesionParameter = 3 - 2*radius; while (y >= x) { ImageDrawPixel(dst, centerX + x, centerY + y, color); ImageDrawPixel(dst, centerX - x, centerY + y, color); ImageDrawPixel(dst, centerX + x, centerY - y, color); ImageDrawPixel(dst, centerX - x, centerY - y, color); ImageDrawPixel(dst, centerX + y, centerY + x, color); ImageDrawPixel(dst, centerX - y, centerY + x, color); ImageDrawPixel(dst, centerX + y, centerY - x, color); ImageDrawPixel(dst, centerX - y, centerY - x, color); x++; if (decesionParameter > 0) { y--; decesionParameter = decesionParameter + 4*(x - y) + 10; } else decesionParameter = decesionParameter + 4*x + 6; } } // Draw circle within an image (Vector version) void ImageDrawCircleV(Image *dst, Vector2 center, int radius, Color color) { ImageDrawCircle(dst, (int)center.x, (int)center.y, radius, color); } // Draw rectangle within an image void ImageDrawRectangle(Image *dst, int posX, int posY, int width, int height, Color color) { ImageDrawRectangleRec(dst, (Rectangle){ (float)posX, (float)posY, (float)width, (float)height }, color); } // Draw rectangle within an image (Vector version) void ImageDrawRectangleV(Image *dst, Vector2 position, Vector2 size, Color color) { ImageDrawRectangle(dst, (int)position.x, (int)position.y, (int)size.x, (int)size.y, color); } // Draw rectangle within an image void ImageDrawRectangleRec(Image *dst, Rectangle rec, Color color) { // Security check to avoid program crash if ((dst->data == NULL) || (dst->width == 0) || (dst->height == 0)) return; int sy = (int)rec.y; int ey = sy + (int)rec.height; int sx = (int)rec.x; int ex = sx + (int)rec.width; for (int y = sy; y < ey; y++) { for (int x = sx; x < ex; x++) { ImageDrawPixel(dst, x, y, color); } } } // Draw rectangle lines within an image void ImageDrawRectangleLines(Image *dst, Rectangle rec, int thick, Color color) { ImageDrawRectangle(dst, (int)rec.x, (int)rec.y, (int)rec.width, thick, color); ImageDrawRectangle(dst, (int)rec.x, (int)(rec.y + thick), thick, (int)(rec.height - thick*2), color); ImageDrawRectangle(dst, (int)(rec.x + rec.width - thick), (int)(rec.y + thick), thick, (int)(rec.height - thick*2), color); ImageDrawRectangle(dst, (int)rec.x, (int)(rec.y + rec.height - thick), (int)rec.width, thick, color); } // Draw an image (source) within an image (destination) // NOTE: Color tint is applied to source image void ImageDraw(Image *dst, Image src, Rectangle srcRec, Rectangle dstRec, Color tint) { // Security check to avoid program crash if ((dst->data == NULL) || (dst->width == 0) || (dst->height == 0) || (src.data == NULL) || (src.width == 0) || (src.height == 0)) return; if (dst->mipmaps > 1) TRACELOG(LOG_WARNING, "Image drawing only applied to base mipmap level"); if (dst->format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "Image drawing not supported for compressed formats"); else { Image srcMod = { 0 }; // Source copy (in case it was required) Image *srcPtr = &src; // Pointer to source image bool useSrcMod = false; // Track source copy required // Source rectangle out-of-bounds security checks if (srcRec.x < 0) { srcRec.width += srcRec.x; srcRec.x = 0; } if (srcRec.y < 0) { srcRec.height += srcRec.y; srcRec.y = 0; } if ((srcRec.x + srcRec.width) > src.width) srcRec.width = src.width - srcRec.x; if ((srcRec.y + srcRec.height) > src.height) srcRec.height = src.height - srcRec.y; // Check if source rectangle needs to be resized to destination rectangle // In that case, we make a copy of source and we apply all required transform if (((int)srcRec.width != (int)dstRec.width) || ((int)srcRec.height != (int)dstRec.height)) { srcMod = ImageFromImage(src, srcRec); // Create image from another image ImageResize(&srcMod, (int)dstRec.width, (int)dstRec.height); // Resize to destination rectangle srcRec = (Rectangle){ 0, 0, (float)srcMod.width, (float)srcMod.height }; srcPtr = &srcMod; useSrcMod = true; } // Destination rectangle out-of-bounds security checks if (dstRec.x < 0) { srcRec.x = -dstRec.x; srcRec.width += dstRec.x; dstRec.x = 0; } else if ((dstRec.x + srcRec.width) > dst->width) srcRec.width = dst->width - dstRec.x; if (dstRec.y < 0) { srcRec.y = -dstRec.y; srcRec.height += dstRec.y; dstRec.y = 0; } else if ((dstRec.y + srcRec.height) > dst->height) srcRec.height = dst->height - dstRec.y; if (dst->width < srcRec.width) srcRec.width = (float)dst->width; if (dst->height < srcRec.height) srcRec.height = (float)dst->height; // This blitting method is quite fast! The process followed is: // for every pixel -> [get_src_format/get_dst_format -> blend -> format_to_dst] // Some optimization ideas: // [x] Avoid creating source copy if not required (no resize required) // [x] Optimize ImageResize() for pixel format (alternative: ImageResizeNN()) // [x] Optimize ColorAlphaBlend() to avoid processing (alpha = 0) and (alpha = 1) // [x] Optimize ColorAlphaBlend() for faster operations (maybe avoiding divs?) // [x] Consider fast path: no alpha blending required cases (src has no alpha) // [x] Consider fast path: same src/dst format with no alpha -> direct line copy // [-] GetPixelColor(): Get Vector4 instead of Color, easier for ColorAlphaBlend() // [ ] Support f32bit channels drawing // TODO: Support PIXELFORMAT_UNCOMPRESSED_R32, PIXELFORMAT_UNCOMPRESSED_R32G32B32, PIXELFORMAT_UNCOMPRESSED_R32G32B32A32 Color colSrc, colDst, blend; bool blendRequired = true; // Fast path: Avoid blend if source has no alpha to blend if ((tint.a == 255) && ((srcPtr->format == PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) || (srcPtr->format == PIXELFORMAT_UNCOMPRESSED_R8G8B8) || (srcPtr->format == PIXELFORMAT_UNCOMPRESSED_R5G6B5))) blendRequired = false; int strideDst = GetPixelDataSize(dst->width, 1, dst->format); int bytesPerPixelDst = strideDst/(dst->width); int strideSrc = GetPixelDataSize(srcPtr->width, 1, srcPtr->format); int bytesPerPixelSrc = strideSrc/(srcPtr->width); unsigned char *pSrcBase = (unsigned char *)srcPtr->data + ((int)srcRec.y*srcPtr->width + (int)srcRec.x)*bytesPerPixelSrc; unsigned char *pDstBase = (unsigned char *)dst->data + ((int)dstRec.y*dst->width + (int)dstRec.x)*bytesPerPixelDst; for (int y = 0; y < (int)srcRec.height; y++) { unsigned char *pSrc = pSrcBase; unsigned char *pDst = pDstBase; // Fast path: Avoid moving pixel by pixel if no blend required and same format if (!blendRequired && (srcPtr->format == dst->format)) memcpy(pDst, pSrc, (int)(srcRec.width)*bytesPerPixelSrc); else { for (int x = 0; x < (int)srcRec.width; x++) { colSrc = GetPixelColor(pSrc, srcPtr->format); colDst = GetPixelColor(pDst, dst->format); // Fast path: Avoid blend if source has no alpha to blend if (blendRequired) blend = ColorAlphaBlend(colDst, colSrc, tint); else blend = colSrc; SetPixelColor(pDst, blend, dst->format); pDst += bytesPerPixelDst; pSrc += bytesPerPixelSrc; } } pSrcBase += strideSrc; pDstBase += strideDst; } if (useSrcMod) UnloadImage(srcMod); // Unload source modified image } } // Draw text (default font) within an image (destination) void ImageDrawText(Image *dst, const char *text, int posX, int posY, int fontSize, Color color) { #if defined(SUPPORT_MODULE_RTEXT) Vector2 position = { (float)posX, (float)posY }; // NOTE: For default font, spacing is set to desired font size / default font size (10) ImageDrawTextEx(dst, GetFontDefault(), text, position, (float)fontSize, (float)fontSize/10, color); // WARNING: Module required: rtext #else TRACELOG(LOG_WARNING, "IMAGE: ImageDrawText() requires module: rtext"); #endif } // Draw text (custom sprite font) within an image (destination) void ImageDrawTextEx(Image *dst, Font font, const char *text, Vector2 position, float fontSize, float spacing, Color tint) { Image imText = ImageTextEx(font, text, fontSize, spacing, tint); Rectangle srcRec = { 0.0f, 0.0f, (float)imText.width, (float)imText.height }; Rectangle dstRec = { position.x, position.y, (float)imText.width, (float)imText.height }; ImageDraw(dst, imText, srcRec, dstRec, WHITE); UnloadImage(imText); } //------------------------------------------------------------------------------------ // Texture loading functions //------------------------------------------------------------------------------------ // Load texture from file into GPU memory (VRAM) Texture2D LoadTexture(const char *fileName) { Texture2D texture = { 0 }; Image image = LoadImage(fileName); if (image.data != NULL) { texture = LoadTextureFromImage(image); UnloadImage(image); } return texture; } // Load a texture from image data // NOTE: image is not unloaded, it must be done manually Texture2D LoadTextureFromImage(Image image) { Texture2D texture = { 0 }; if ((image.width != 0) && (image.height != 0)) { texture.id = rlLoadTexture(image.data, image.width, image.height, image.format, image.mipmaps); } else TRACELOG(LOG_WARNING, "IMAGE: Data is not valid to load texture"); texture.width = image.width; texture.height = image.height; texture.mipmaps = image.mipmaps; texture.format = image.format; return texture; } // Load cubemap from image, multiple image cubemap layouts supported TextureCubemap LoadTextureCubemap(Image image, int layout) { TextureCubemap cubemap = { 0 }; if (layout == CUBEMAP_LAYOUT_AUTO_DETECT) // Try to automatically guess layout type { // Check image width/height to determine the type of cubemap provided if (image.width > image.height) { if ((image.width/6) == image.height) { layout = CUBEMAP_LAYOUT_LINE_HORIZONTAL; cubemap.width = image.width/6; } else if ((image.width/4) == (image.height/3)) { layout = CUBEMAP_LAYOUT_CROSS_FOUR_BY_THREE; cubemap.width = image.width/4; } else if (image.width >= (int)((float)image.height*1.85f)) { layout = CUBEMAP_LAYOUT_PANORAMA; cubemap.width = image.width/4; } } else if (image.height > image.width) { if ((image.height/6) == image.width) { layout = CUBEMAP_LAYOUT_LINE_VERTICAL; cubemap.width = image.height/6; } else if ((image.width/3) == (image.height/4)) { layout = CUBEMAP_LAYOUT_CROSS_THREE_BY_FOUR; cubemap.width = image.width/3; } } cubemap.height = cubemap.width; } // Layout provided or already auto-detected if (layout != CUBEMAP_LAYOUT_AUTO_DETECT) { int size = cubemap.width; Image faces = { 0 }; // Vertical column image Rectangle faceRecs[6] = { 0 }; // Face source rectangles for (int i = 0; i < 6; i++) faceRecs[i] = (Rectangle){ 0, 0, (float)size, (float)size }; if (layout == CUBEMAP_LAYOUT_LINE_VERTICAL) { faces = ImageCopy(image); // Image data already follows expected convention } else if (layout == CUBEMAP_LAYOUT_PANORAMA) { // TODO: Convert panorama image to square faces... // Ref: https://github.com/denivip/panorama/blob/master/panorama.cpp } else { if (layout == CUBEMAP_LAYOUT_LINE_HORIZONTAL) for (int i = 0; i < 6; i++) faceRecs[i].x = (float)size*i; else if (layout == CUBEMAP_LAYOUT_CROSS_THREE_BY_FOUR) { faceRecs[0].x = (float)size; faceRecs[0].y = (float)size; faceRecs[1].x = (float)size; faceRecs[1].y = (float)size*3; faceRecs[2].x = (float)size; faceRecs[2].y = 0; faceRecs[3].x = (float)size; faceRecs[3].y = (float)size*2; faceRecs[4].x = 0; faceRecs[4].y = (float)size; faceRecs[5].x = (float)size*2; faceRecs[5].y = (float)size; } else if (layout == CUBEMAP_LAYOUT_CROSS_FOUR_BY_THREE) { faceRecs[0].x = (float)size*2; faceRecs[0].y = (float)size; faceRecs[1].x = 0; faceRecs[1].y = (float)size; faceRecs[2].x = (float)size; faceRecs[2].y = 0; faceRecs[3].x = (float)size; faceRecs[3].y = (float)size*2; faceRecs[4].x = (float)size; faceRecs[4].y = (float)size; faceRecs[5].x = (float)size*3; faceRecs[5].y = (float)size; } // Convert image data to 6 faces in a vertical column, that's the optimum layout for loading faces = GenImageColor(size, size*6, MAGENTA); ImageFormat(&faces, image.format); // NOTE: Image formating does not work with compressed textures for (int i = 0; i < 6; i++) ImageDraw(&faces, image, faceRecs[i], (Rectangle){ 0, (float)size*i, (float)size, (float)size }, WHITE); } // NOTE: Cubemap data is expected to be provided as 6 images in a single data array, // one after the other (that's a vertical image), following convention: +X, -X, +Y, -Y, +Z, -Z cubemap.id = rlLoadTextureCubemap(faces.data, size, faces.format); if (cubemap.id == 0) TRACELOG(LOG_WARNING, "IMAGE: Failed to load cubemap image"); UnloadImage(faces); } else TRACELOG(LOG_WARNING, "IMAGE: Failed to detect cubemap image layout"); return cubemap; } // Load texture for rendering (framebuffer) // NOTE: Render texture is loaded by default with RGBA color attachment and depth RenderBuffer RenderTexture2D LoadRenderTexture(int width, int height) { RenderTexture2D target = { 0 }; target.id = rlLoadFramebuffer(width, height); // Load an empty framebuffer if (target.id > 0) { rlEnableFramebuffer(target.id); // Create color texture (default to RGBA) target.texture.id = rlLoadTexture(NULL, width, height, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 1); target.texture.width = width; target.texture.height = height; target.texture.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; target.texture.mipmaps = 1; // Create depth renderbuffer/texture target.depth.id = rlLoadTextureDepth(width, height, true); target.depth.width = width; target.depth.height = height; target.depth.format = 19; //DEPTH_COMPONENT_24BIT? target.depth.mipmaps = 1; // Attach color texture and depth renderbuffer/texture to FBO rlFramebufferAttach(target.id, target.texture.id, RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_TEXTURE2D, 0); rlFramebufferAttach(target.id, target.depth.id, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_RENDERBUFFER, 0); // Check if fbo is complete with attachments (valid) if (rlFramebufferComplete(target.id)) TRACELOG(LOG_INFO, "FBO: [ID %i] Framebuffer object created successfully", target.id); rlDisableFramebuffer(); } else TRACELOG(LOG_WARNING, "FBO: Framebuffer object can not be created"); return target; } // Unload texture from GPU memory (VRAM) void UnloadTexture(Texture2D texture) { if (texture.id > 0) { rlUnloadTexture(texture.id); TRACELOG(LOG_INFO, "TEXTURE: [ID %i] Unloaded texture data from VRAM (GPU)", texture.id); } } // Unload render texture from GPU memory (VRAM) void UnloadRenderTexture(RenderTexture2D target) { if (target.id > 0) { // Color texture attached to FBO is deleted rlUnloadTexture(target.texture.id); // NOTE: Depth texture/renderbuffer is automatically // queried and deleted before deleting framebuffer rlUnloadFramebuffer(target.id); } } // Update GPU texture with new data // NOTE: pixels data must match texture.format void UpdateTexture(Texture2D texture, const void *pixels) { rlUpdateTexture(texture.id, 0, 0, texture.width, texture.height, texture.format, pixels); } // Update GPU texture rectangle with new data // NOTE: pixels data must match texture.format void UpdateTextureRec(Texture2D texture, Rectangle rec, const void *pixels) { rlUpdateTexture(texture.id, (int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, texture.format, pixels); } //------------------------------------------------------------------------------------ // Texture configuration functions //------------------------------------------------------------------------------------ // Generate GPU mipmaps for a texture void GenTextureMipmaps(Texture2D *texture) { // NOTE: NPOT textures support check inside function // On WebGL (OpenGL ES 2.0) NPOT textures support is limited rlGenTextureMipmaps(texture->id, texture->width, texture->height, texture->format, &texture->mipmaps); } // Set texture scaling filter mode void SetTextureFilter(Texture2D texture, int filter) { switch (filter) { case TEXTURE_FILTER_POINT: { if (texture.mipmaps > 1) { // RL_TEXTURE_FILTER_MIP_NEAREST - tex filter: POINT, mipmaps filter: POINT (sharp switching between mipmaps) rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_MIP_NEAREST); // RL_TEXTURE_FILTER_NEAREST - tex filter: POINT (no filter), no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_NEAREST); } else { // RL_TEXTURE_FILTER_NEAREST - tex filter: POINT (no filter), no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_NEAREST); rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_NEAREST); } } break; case TEXTURE_FILTER_BILINEAR: { if (texture.mipmaps > 1) { // RL_TEXTURE_FILTER_LINEAR_MIP_NEAREST - tex filter: BILINEAR, mipmaps filter: POINT (sharp switching between mipmaps) // Alternative: RL_TEXTURE_FILTER_NEAREST_MIP_LINEAR - tex filter: POINT, mipmaps filter: BILINEAR (smooth transition between mipmaps) rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_LINEAR_MIP_NEAREST); // RL_TEXTURE_FILTER_LINEAR - tex filter: BILINEAR, no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_LINEAR); } else { // RL_TEXTURE_FILTER_LINEAR - tex filter: BILINEAR, no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_LINEAR); rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_LINEAR); } } break; case TEXTURE_FILTER_TRILINEAR: { if (texture.mipmaps > 1) { // RL_TEXTURE_FILTER_MIP_LINEAR - tex filter: BILINEAR, mipmaps filter: BILINEAR (smooth transition between mipmaps) rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_MIP_LINEAR); // RL_TEXTURE_FILTER_LINEAR - tex filter: BILINEAR, no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_LINEAR); } else { TRACELOG(LOG_WARNING, "TEXTURE: [ID %i] No mipmaps available for TRILINEAR texture filtering", texture.id); // RL_TEXTURE_FILTER_LINEAR - tex filter: BILINEAR, no mipmaps rlTextureParameters(texture.id, RL_TEXTURE_MIN_FILTER, RL_TEXTURE_FILTER_LINEAR); rlTextureParameters(texture.id, RL_TEXTURE_MAG_FILTER, RL_TEXTURE_FILTER_LINEAR); } } break; case TEXTURE_FILTER_ANISOTROPIC_4X: rlTextureParameters(texture.id, RL_TEXTURE_FILTER_ANISOTROPIC, 4); break; case TEXTURE_FILTER_ANISOTROPIC_8X: rlTextureParameters(texture.id, RL_TEXTURE_FILTER_ANISOTROPIC, 8); break; case TEXTURE_FILTER_ANISOTROPIC_16X: rlTextureParameters(texture.id, RL_TEXTURE_FILTER_ANISOTROPIC, 16); break; default: break; } } // Set texture wrapping mode void SetTextureWrap(Texture2D texture, int wrap) { switch (wrap) { case TEXTURE_WRAP_REPEAT: { // NOTE: It only works if NPOT textures are supported, i.e. OpenGL ES 2.0 could not support it rlTextureParameters(texture.id, RL_TEXTURE_WRAP_S, RL_TEXTURE_WRAP_REPEAT); rlTextureParameters(texture.id, RL_TEXTURE_WRAP_T, RL_TEXTURE_WRAP_REPEAT); } break; case TEXTURE_WRAP_CLAMP: { rlTextureParameters(texture.id, RL_TEXTURE_WRAP_S, RL_TEXTURE_WRAP_CLAMP); rlTextureParameters(texture.id, RL_TEXTURE_WRAP_T, RL_TEXTURE_WRAP_CLAMP); } break; case TEXTURE_WRAP_MIRROR_REPEAT: { rlTextureParameters(texture.id, RL_TEXTURE_WRAP_S, RL_TEXTURE_WRAP_MIRROR_REPEAT); rlTextureParameters(texture.id, RL_TEXTURE_WRAP_T, RL_TEXTURE_WRAP_MIRROR_REPEAT); } break; case TEXTURE_WRAP_MIRROR_CLAMP: { rlTextureParameters(texture.id, RL_TEXTURE_WRAP_S, RL_TEXTURE_WRAP_MIRROR_CLAMP); rlTextureParameters(texture.id, RL_TEXTURE_WRAP_T, RL_TEXTURE_WRAP_MIRROR_CLAMP); } break; default: break; } } //------------------------------------------------------------------------------------ // Texture drawing functions //------------------------------------------------------------------------------------ // Draw a Texture2D void DrawTexture(Texture2D texture, int posX, int posY, Color tint) { DrawTextureEx(texture, (Vector2){ (float)posX, (float)posY }, 0.0f, 1.0f, tint); } // Draw a Texture2D with position defined as Vector2 void DrawTextureV(Texture2D texture, Vector2 position, Color tint) { DrawTextureEx(texture, position, 0, 1.0f, tint); } // Draw a Texture2D with extended parameters void DrawTextureEx(Texture2D texture, Vector2 position, float rotation, float scale, Color tint) { Rectangle source = { 0.0f, 0.0f, (float)texture.width, (float)texture.height }; Rectangle dest = { position.x, position.y, (float)texture.width*scale, (float)texture.height*scale }; Vector2 origin = { 0.0f, 0.0f }; DrawTexturePro(texture, source, dest, origin, rotation, tint); } // Draw a part of a texture (defined by a rectangle) void DrawTextureRec(Texture2D texture, Rectangle source, Vector2 position, Color tint) { Rectangle dest = { position.x, position.y, fabsf(source.width), fabsf(source.height) }; Vector2 origin = { 0.0f, 0.0f }; DrawTexturePro(texture, source, dest, origin, 0.0f, tint); } // Draw texture quad with tiling and offset parameters // NOTE: Tiling and offset should be provided considering normalized texture values [0..1] // i.e tiling = { 1.0f, 1.0f } refers to all texture, offset = { 0.5f, 0.5f } moves texture origin to center void DrawTextureQuad(Texture2D texture, Vector2 tiling, Vector2 offset, Rectangle quad, Color tint) { // WARNING: This solution only works if TEXTURE_WRAP_REPEAT is supported, // NPOT textures supported is required and OpenGL ES 2.0 could not support it Rectangle source = { offset.x*texture.width, offset.y*texture.height, tiling.x*texture.width, tiling.y*texture.height }; Vector2 origin = { 0.0f, 0.0f }; DrawTexturePro(texture, source, quad, origin, 0.0f, tint); } // Draw part of a texture (defined by a rectangle) with rotation and scale tiled into dest. // NOTE: For tilling a whole texture DrawTextureQuad() is better void DrawTextureTiled(Texture2D texture, Rectangle source, Rectangle dest, Vector2 origin, float rotation, float scale, Color tint) { if ((texture.id <= 0) || (scale <= 0.0f)) return; // Wanna see a infinite loop?!...just delete this line! if ((source.width == 0) || (source.height == 0)) return; int tileWidth = (int)(source.width*scale), tileHeight = (int)(source.height*scale); if ((dest.width < tileWidth) && (dest.height < tileHeight)) { // Can fit only one tile DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)dest.width/tileWidth)*source.width, ((float)dest.height/tileHeight)*source.height}, (Rectangle){dest.x, dest.y, dest.width, dest.height}, origin, rotation, tint); } else if (dest.width <= tileWidth) { // Tiled vertically (one column) int dy = 0; for (;dy+tileHeight < dest.height; dy += tileHeight) { DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)dest.width/tileWidth)*source.width, source.height}, (Rectangle){dest.x, dest.y + dy, dest.width, (float)tileHeight}, origin, rotation, tint); } // Fit last tile if (dy < dest.height) { DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)dest.width/tileWidth)*source.width, ((float)(dest.height - dy)/tileHeight)*source.height}, (Rectangle){dest.x, dest.y + dy, dest.width, dest.height - dy}, origin, rotation, tint); } } else if (dest.height <= tileHeight) { // Tiled horizontally (one row) int dx = 0; for (;dx+tileWidth < dest.width; dx += tileWidth) { DrawTexturePro(texture, (Rectangle){source.x, source.y, source.width, ((float)dest.height/tileHeight)*source.height}, (Rectangle){dest.x + dx, dest.y, (float)tileWidth, dest.height}, origin, rotation, tint); } // Fit last tile if (dx < dest.width) { DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)(dest.width - dx)/tileWidth)*source.width, ((float)dest.height/tileHeight)*source.height}, (Rectangle){dest.x + dx, dest.y, dest.width - dx, dest.height}, origin, rotation, tint); } } else { // Tiled both horizontally and vertically (rows and columns) int dx = 0; for (;dx+tileWidth < dest.width; dx += tileWidth) { int dy = 0; for (;dy+tileHeight < dest.height; dy += tileHeight) { DrawTexturePro(texture, source, (Rectangle){dest.x + dx, dest.y + dy, (float)tileWidth, (float)tileHeight}, origin, rotation, tint); } if (dy < dest.height) { DrawTexturePro(texture, (Rectangle){source.x, source.y, source.width, ((float)(dest.height - dy)/tileHeight)*source.height}, (Rectangle){dest.x + dx, dest.y + dy, (float)tileWidth, dest.height - dy}, origin, rotation, tint); } } // Fit last column of tiles if (dx < dest.width) { int dy = 0; for (;dy+tileHeight < dest.height; dy += tileHeight) { DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)(dest.width - dx)/tileWidth)*source.width, source.height}, (Rectangle){dest.x + dx, dest.y + dy, dest.width - dx, (float)tileHeight}, origin, rotation, tint); } // Draw final tile in the bottom right corner if (dy < dest.height) { DrawTexturePro(texture, (Rectangle){source.x, source.y, ((float)(dest.width - dx)/tileWidth)*source.width, ((float)(dest.height - dy)/tileHeight)*source.height}, (Rectangle){dest.x + dx, dest.y + dy, dest.width - dx, dest.height - dy}, origin, rotation, tint); } } } } // Draw a part of a texture (defined by a rectangle) with 'pro' parameters // NOTE: origin is relative to destination rectangle size void DrawTexturePro(Texture2D texture, Rectangle source, Rectangle dest, Vector2 origin, float rotation, Color tint) { // Check if texture is valid if (texture.id > 0) { float width = (float)texture.width; float height = (float)texture.height; bool flipX = false; if (source.width < 0) { flipX = true; source.width *= -1; } if (source.height < 0) source.y -= source.height; Vector2 topLeft = { 0 }; Vector2 topRight = { 0 }; Vector2 bottomLeft = { 0 }; Vector2 bottomRight = { 0 }; // Only calculate rotation if needed if (rotation == 0.0f) { float x = dest.x - origin.x; float y = dest.y - origin.y; topLeft = (Vector2){ x, y }; topRight = (Vector2){ x + dest.width, y }; bottomLeft = (Vector2){ x, y + dest.height }; bottomRight = (Vector2){ x + dest.width, y + dest.height }; } else { float sinRotation = sinf(rotation*DEG2RAD); float cosRotation = cosf(rotation*DEG2RAD); float x = dest.x; float y = dest.y; float dx = -origin.x; float dy = -origin.y; topLeft.x = x + dx*cosRotation - dy*sinRotation; topLeft.y = y + dx*sinRotation + dy*cosRotation; topRight.x = x + (dx + dest.width)*cosRotation - dy*sinRotation; topRight.y = y + (dx + dest.width)*sinRotation + dy*cosRotation; bottomLeft.x = x + dx*cosRotation - (dy + dest.height)*sinRotation; bottomLeft.y = y + dx*sinRotation + (dy + dest.height)*cosRotation; bottomRight.x = x + (dx + dest.width)*cosRotation - (dy + dest.height)*sinRotation; bottomRight.y = y + (dx + dest.width)*sinRotation + (dy + dest.height)*cosRotation; } rlCheckRenderBatchLimit(4); // Make sure there is enough free space on the batch buffer rlSetTexture(texture.id); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer // Top-left corner for texture and quad if (flipX) rlTexCoord2f((source.x + source.width)/width, source.y/height); else rlTexCoord2f(source.x/width, source.y/height); rlVertex2f(topLeft.x, topLeft.y); // Bottom-left corner for texture and quad if (flipX) rlTexCoord2f((source.x + source.width)/width, (source.y + source.height)/height); else rlTexCoord2f(source.x/width, (source.y + source.height)/height); rlVertex2f(bottomLeft.x, bottomLeft.y); // Bottom-right corner for texture and quad if (flipX) rlTexCoord2f(source.x/width, (source.y + source.height)/height); else rlTexCoord2f((source.x + source.width)/width, (source.y + source.height)/height); rlVertex2f(bottomRight.x, bottomRight.y); // Top-right corner for texture and quad if (flipX) rlTexCoord2f(source.x/width, source.y/height); else rlTexCoord2f((source.x + source.width)/width, source.y/height); rlVertex2f(topRight.x, topRight.y); rlEnd(); rlSetTexture(0); // NOTE: Vertex position can be transformed using matrices // but the process is way more costly than just calculating // the vertex positions manually, like done above. // I leave here the old implementation for educational pourposes, // just in case someone wants to do some performance test /* rlSetTexture(texture.id); rlPushMatrix(); rlTranslatef(dest.x, dest.y, 0.0f); if (rotation != 0.0f) rlRotatef(rotation, 0.0f, 0.0f, 1.0f); rlTranslatef(-origin.x, -origin.y, 0.0f); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer // Bottom-left corner for texture and quad if (flipX) rlTexCoord2f((source.x + source.width)/width, source.y/height); else rlTexCoord2f(source.x/width, source.y/height); rlVertex2f(0.0f, 0.0f); // Bottom-right corner for texture and quad if (flipX) rlTexCoord2f((source.x + source.width)/width, (source.y + source.height)/height); else rlTexCoord2f(source.x/width, (source.y + source.height)/height); rlVertex2f(0.0f, dest.height); // Top-right corner for texture and quad if (flipX) rlTexCoord2f(source.x/width, (source.y + source.height)/height); else rlTexCoord2f((source.x + source.width)/width, (source.y + source.height)/height); rlVertex2f(dest.width, dest.height); // Top-left corner for texture and quad if (flipX) rlTexCoord2f(source.x/width, source.y/height); else rlTexCoord2f((source.x + source.width)/width, source.y/height); rlVertex2f(dest.width, 0.0f); rlEnd(); rlPopMatrix(); rlSetTexture(0); */ } } // Draws a texture (or part of it) that stretches or shrinks nicely using n-patch info void DrawTextureNPatch(Texture2D texture, NPatchInfo nPatchInfo, Rectangle dest, Vector2 origin, float rotation, Color tint) { if (texture.id > 0) { float width = (float)texture.width; float height = (float)texture.height; float patchWidth = ((int)dest.width <= 0)? 0.0f : dest.width; float patchHeight = ((int)dest.height <= 0)? 0.0f : dest.height; if (nPatchInfo.source.width < 0) nPatchInfo.source.x -= nPatchInfo.source.width; if (nPatchInfo.source.height < 0) nPatchInfo.source.y -= nPatchInfo.source.height; if (nPatchInfo.layout == NPATCH_THREE_PATCH_HORIZONTAL) patchHeight = nPatchInfo.source.height; if (nPatchInfo.layout == NPATCH_THREE_PATCH_VERTICAL) patchWidth = nPatchInfo.source.width; bool drawCenter = true; bool drawMiddle = true; float leftBorder = (float)nPatchInfo.left; float topBorder = (float)nPatchInfo.top; float rightBorder = (float)nPatchInfo.right; float bottomBorder = (float)nPatchInfo.bottom; // Adjust the lateral (left and right) border widths in case patchWidth < texture.width if (patchWidth <= (leftBorder + rightBorder) && nPatchInfo.layout != NPATCH_THREE_PATCH_VERTICAL) { drawCenter = false; leftBorder = (leftBorder/(leftBorder + rightBorder))*patchWidth; rightBorder = patchWidth - leftBorder; } // Adjust the lateral (top and bottom) border heights in case patchHeight < texture.height if (patchHeight <= (topBorder + bottomBorder) && nPatchInfo.layout != NPATCH_THREE_PATCH_HORIZONTAL) { drawMiddle = false; topBorder = (topBorder/(topBorder + bottomBorder))*patchHeight; bottomBorder = patchHeight - topBorder; } Vector2 vertA, vertB, vertC, vertD; vertA.x = 0.0f; // outer left vertA.y = 0.0f; // outer top vertB.x = leftBorder; // inner left vertB.y = topBorder; // inner top vertC.x = patchWidth - rightBorder; // inner right vertC.y = patchHeight - bottomBorder; // inner bottom vertD.x = patchWidth; // outer right vertD.y = patchHeight; // outer bottom Vector2 coordA, coordB, coordC, coordD; coordA.x = nPatchInfo.source.x/width; coordA.y = nPatchInfo.source.y/height; coordB.x = (nPatchInfo.source.x + leftBorder)/width; coordB.y = (nPatchInfo.source.y + topBorder)/height; coordC.x = (nPatchInfo.source.x + nPatchInfo.source.width - rightBorder)/width; coordC.y = (nPatchInfo.source.y + nPatchInfo.source.height - bottomBorder)/height; coordD.x = (nPatchInfo.source.x + nPatchInfo.source.width)/width; coordD.y = (nPatchInfo.source.y + nPatchInfo.source.height)/height; rlSetTexture(texture.id); rlPushMatrix(); rlTranslatef(dest.x, dest.y, 0.0f); rlRotatef(rotation, 0.0f, 0.0f, 1.0f); rlTranslatef(-origin.x, -origin.y, 0.0f); rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); rlNormal3f(0.0f, 0.0f, 1.0f); // Normal vector pointing towards viewer if (nPatchInfo.layout == NPATCH_NINE_PATCH) { // ------------------------------------------------------------ // TOP-LEFT QUAD rlTexCoord2f(coordA.x, coordB.y); rlVertex2f(vertA.x, vertB.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordB.x, coordB.y); rlVertex2f(vertB.x, vertB.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordB.x, coordA.y); rlVertex2f(vertB.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordA.y); rlVertex2f(vertA.x, vertA.y); // Top-left corner for texture and quad if (drawCenter) { // TOP-CENTER QUAD rlTexCoord2f(coordB.x, coordB.y); rlVertex2f(vertB.x, vertB.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordC.x, coordB.y); rlVertex2f(vertC.x, vertB.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordC.x, coordA.y); rlVertex2f(vertC.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordB.x, coordA.y); rlVertex2f(vertB.x, vertA.y); // Top-left corner for texture and quad } // TOP-RIGHT QUAD rlTexCoord2f(coordC.x, coordB.y); rlVertex2f(vertC.x, vertB.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordB.y); rlVertex2f(vertD.x, vertB.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordA.y); rlVertex2f(vertD.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordC.x, coordA.y); rlVertex2f(vertC.x, vertA.y); // Top-left corner for texture and quad if (drawMiddle) { // ------------------------------------------------------------ // MIDDLE-LEFT QUAD rlTexCoord2f(coordA.x, coordC.y); rlVertex2f(vertA.x, vertC.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordB.x, coordC.y); rlVertex2f(vertB.x, vertC.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordB.x, coordB.y); rlVertex2f(vertB.x, vertB.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordB.y); rlVertex2f(vertA.x, vertB.y); // Top-left corner for texture and quad if (drawCenter) { // MIDDLE-CENTER QUAD rlTexCoord2f(coordB.x, coordC.y); rlVertex2f(vertB.x, vertC.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordC.x, coordC.y); rlVertex2f(vertC.x, vertC.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordC.x, coordB.y); rlVertex2f(vertC.x, vertB.y); // Top-right corner for texture and quad rlTexCoord2f(coordB.x, coordB.y); rlVertex2f(vertB.x, vertB.y); // Top-left corner for texture and quad } // MIDDLE-RIGHT QUAD rlTexCoord2f(coordC.x, coordC.y); rlVertex2f(vertC.x, vertC.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordC.y); rlVertex2f(vertD.x, vertC.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordB.y); rlVertex2f(vertD.x, vertB.y); // Top-right corner for texture and quad rlTexCoord2f(coordC.x, coordB.y); rlVertex2f(vertC.x, vertB.y); // Top-left corner for texture and quad } // ------------------------------------------------------------ // BOTTOM-LEFT QUAD rlTexCoord2f(coordA.x, coordD.y); rlVertex2f(vertA.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordB.x, coordD.y); rlVertex2f(vertB.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordB.x, coordC.y); rlVertex2f(vertB.x, vertC.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordC.y); rlVertex2f(vertA.x, vertC.y); // Top-left corner for texture and quad if (drawCenter) { // BOTTOM-CENTER QUAD rlTexCoord2f(coordB.x, coordD.y); rlVertex2f(vertB.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordC.x, coordD.y); rlVertex2f(vertC.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordC.x, coordC.y); rlVertex2f(vertC.x, vertC.y); // Top-right corner for texture and quad rlTexCoord2f(coordB.x, coordC.y); rlVertex2f(vertB.x, vertC.y); // Top-left corner for texture and quad } // BOTTOM-RIGHT QUAD rlTexCoord2f(coordC.x, coordD.y); rlVertex2f(vertC.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordD.y); rlVertex2f(vertD.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordC.y); rlVertex2f(vertD.x, vertC.y); // Top-right corner for texture and quad rlTexCoord2f(coordC.x, coordC.y); rlVertex2f(vertC.x, vertC.y); // Top-left corner for texture and quad } else if (nPatchInfo.layout == NPATCH_THREE_PATCH_VERTICAL) { // TOP QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordA.x, coordB.y); rlVertex2f(vertA.x, vertB.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordB.y); rlVertex2f(vertD.x, vertB.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordA.y); rlVertex2f(vertD.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordA.y); rlVertex2f(vertA.x, vertA.y); // Top-left corner for texture and quad if (drawCenter) { // MIDDLE QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordA.x, coordC.y); rlVertex2f(vertA.x, vertC.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordC.y); rlVertex2f(vertD.x, vertC.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordB.y); rlVertex2f(vertD.x, vertB.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordB.y); rlVertex2f(vertA.x, vertB.y); // Top-left corner for texture and quad } // BOTTOM QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordA.x, coordD.y); rlVertex2f(vertA.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordD.y); rlVertex2f(vertD.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordC.y); rlVertex2f(vertD.x, vertC.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordC.y); rlVertex2f(vertA.x, vertC.y); // Top-left corner for texture and quad } else if (nPatchInfo.layout == NPATCH_THREE_PATCH_HORIZONTAL) { // LEFT QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordA.x, coordD.y); rlVertex2f(vertA.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordB.x, coordD.y); rlVertex2f(vertB.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordB.x, coordA.y); rlVertex2f(vertB.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordA.x, coordA.y); rlVertex2f(vertA.x, vertA.y); // Top-left corner for texture and quad if (drawCenter) { // CENTER QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordB.x, coordD.y); rlVertex2f(vertB.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordC.x, coordD.y); rlVertex2f(vertC.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordC.x, coordA.y); rlVertex2f(vertC.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordB.x, coordA.y); rlVertex2f(vertB.x, vertA.y); // Top-left corner for texture and quad } // RIGHT QUAD // ----------------------------------------------------------- // Texture coords Vertices rlTexCoord2f(coordC.x, coordD.y); rlVertex2f(vertC.x, vertD.y); // Bottom-left corner for texture and quad rlTexCoord2f(coordD.x, coordD.y); rlVertex2f(vertD.x, vertD.y); // Bottom-right corner for texture and quad rlTexCoord2f(coordD.x, coordA.y); rlVertex2f(vertD.x, vertA.y); // Top-right corner for texture and quad rlTexCoord2f(coordC.x, coordA.y); rlVertex2f(vertC.x, vertA.y); // Top-left corner for texture and quad } rlEnd(); rlPopMatrix(); rlSetTexture(0); } } // Draw textured polygon, defined by vertex and texturecoordinates // NOTE: Polygon center must have straight line path to all points // without crossing perimeter, points must be in anticlockwise order void DrawTexturePoly(Texture2D texture, Vector2 center, Vector2 *points, Vector2 *texcoords, int pointCount, Color tint) { rlCheckRenderBatchLimit((pointCount - 1)*4); rlSetTexture(texture.id); // Texturing is only supported on QUADs rlBegin(RL_QUADS); rlColor4ub(tint.r, tint.g, tint.b, tint.a); for (int i = 0; i < pointCount - 1; i++) { rlTexCoord2f(0.5f, 0.5f); rlVertex2f(center.x, center.y); rlTexCoord2f(texcoords[i].x, texcoords[i].y); rlVertex2f(points[i].x + center.x, points[i].y + center.y); rlTexCoord2f(texcoords[i + 1].x, texcoords[i + 1].y); rlVertex2f(points[i + 1].x + center.x, points[i + 1].y + center.y); rlTexCoord2f(texcoords[i + 1].x, texcoords[i + 1].y); rlVertex2f(points[i + 1].x + center.x, points[i + 1].y + center.y); } rlEnd(); rlSetTexture(0); } // Get color with alpha applied, alpha goes from 0.0f to 1.0f Color Fade(Color color, float alpha) { if (alpha < 0.0f) alpha = 0.0f; else if (alpha > 1.0f) alpha = 1.0f; return (Color){color.r, color.g, color.b, (unsigned char)(255.0f*alpha)}; } // Get hexadecimal value for a Color int ColorToInt(Color color) { return (((int)color.r << 24) | ((int)color.g << 16) | ((int)color.b << 8) | (int)color.a); } // Get color normalized as float [0..1] Vector4 ColorNormalize(Color color) { Vector4 result; result.x = (float)color.r/255.0f; result.y = (float)color.g/255.0f; result.z = (float)color.b/255.0f; result.w = (float)color.a/255.0f; return result; } // Get color from normalized values [0..1] Color ColorFromNormalized(Vector4 normalized) { Color result; result.r = (unsigned char)(normalized.x*255.0f); result.g = (unsigned char)(normalized.y*255.0f); result.b = (unsigned char)(normalized.z*255.0f); result.a = (unsigned char)(normalized.w*255.0f); return result; } // Get HSV values for a Color // NOTE: Hue is returned as degrees [0..360] Vector3 ColorToHSV(Color color) { Vector3 hsv = { 0 }; Vector3 rgb = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; float min, max, delta; min = rgb.x < rgb.y? rgb.x : rgb.y; min = min < rgb.z? min : rgb.z; max = rgb.x > rgb.y? rgb.x : rgb.y; max = max > rgb.z? max : rgb.z; hsv.z = max; // Value delta = max - min; if (delta < 0.00001f) { hsv.y = 0.0f; hsv.x = 0.0f; // Undefined, maybe NAN? return hsv; } if (max > 0.0f) { // NOTE: If max is 0, this divide would cause a crash hsv.y = (delta/max); // Saturation } else { // NOTE: If max is 0, then r = g = b = 0, s = 0, h is undefined hsv.y = 0.0f; hsv.x = NAN; // Undefined return hsv; } // NOTE: Comparing float values could not work properly if (rgb.x >= max) hsv.x = (rgb.y - rgb.z)/delta; // Between yellow & magenta else { if (rgb.y >= max) hsv.x = 2.0f + (rgb.z - rgb.x)/delta; // Between cyan & yellow else hsv.x = 4.0f + (rgb.x - rgb.y)/delta; // Between magenta & cyan } hsv.x *= 60.0f; // Convert to degrees if (hsv.x < 0.0f) hsv.x += 360.0f; return hsv; } // Get a Color from HSV values // Implementation reference: https://en.wikipedia.org/wiki/HSL_and_HSV#Alternative_HSV_conversion // NOTE: Color->HSV->Color conversion will not yield exactly the same color due to rounding errors // Hue is provided in degrees: [0..360] // Saturation/Value are provided normalized: [0.0f..1.0f] Color ColorFromHSV(float hue, float saturation, float value) { Color color = { 0, 0, 0, 255 }; // Red channel float k = fmodf((5.0f + hue/60.0f), 6); float t = 4.0f - k; k = (t < k)? t : k; k = (k < 1)? k : 1; k = (k > 0)? k : 0; color.r = (unsigned char)((value - value*saturation*k)*255.0f); // Green channel k = fmodf((3.0f + hue/60.0f), 6); t = 4.0f - k; k = (t < k)? t : k; k = (k < 1)? k : 1; k = (k > 0)? k : 0; color.g = (unsigned char)((value - value*saturation*k)*255.0f); // Blue channel k = fmodf((1.0f + hue/60.0f), 6); t = 4.0f - k; k = (t < k)? t : k; k = (k < 1)? k : 1; k = (k > 0)? k : 0; color.b = (unsigned char)((value - value*saturation*k)*255.0f); return color; } // Get color with alpha applied, alpha goes from 0.0f to 1.0f Color ColorAlpha(Color color, float alpha) { if (alpha < 0.0f) alpha = 0.0f; else if (alpha > 1.0f) alpha = 1.0f; return (Color){color.r, color.g, color.b, (unsigned char)(255.0f*alpha)}; } // Get src alpha-blended into dst color with tint Color ColorAlphaBlend(Color dst, Color src, Color tint) { Color out = WHITE; // Apply color tint to source color src.r = (unsigned char)(((unsigned int)src.r*(unsigned int)tint.r) >> 8); src.g = (unsigned char)(((unsigned int)src.g*(unsigned int)tint.g) >> 8); src.b = (unsigned char)(((unsigned int)src.b*(unsigned int)tint.b) >> 8); src.a = (unsigned char)(((unsigned int)src.a*(unsigned int)tint.a) >> 8); //#define COLORALPHABLEND_FLOAT #define COLORALPHABLEND_INTEGERS #if defined(COLORALPHABLEND_INTEGERS) if (src.a == 0) out = dst; else if (src.a == 255) out = src; else { unsigned int alpha = (unsigned int)src.a + 1; // We are shifting by 8 (dividing by 256), so we need to take that excess into account out.a = (unsigned char)(((unsigned int)alpha*256 + (unsigned int)dst.a*(256 - alpha)) >> 8); if (out.a > 0) { out.r = (unsigned char)((((unsigned int)src.r*alpha*256 + (unsigned int)dst.r*(unsigned int)dst.a*(256 - alpha))/out.a) >> 8); out.g = (unsigned char)((((unsigned int)src.g*alpha*256 + (unsigned int)dst.g*(unsigned int)dst.a*(256 - alpha))/out.a) >> 8); out.b = (unsigned char)((((unsigned int)src.b*alpha*256 + (unsigned int)dst.b*(unsigned int)dst.a*(256 - alpha))/out.a) >> 8); } } #endif #if defined(COLORALPHABLEND_FLOAT) if (src.a == 0) out = dst; else if (src.a == 255) out = src; else { Vector4 fdst = ColorNormalize(dst); Vector4 fsrc = ColorNormalize(src); Vector4 ftint = ColorNormalize(tint); Vector4 fout = { 0 }; fout.w = fsrc.w + fdst.w*(1.0f - fsrc.w); if (fout.w > 0.0f) { fout.x = (fsrc.x*fsrc.w + fdst.x*fdst.w*(1 - fsrc.w))/fout.w; fout.y = (fsrc.y*fsrc.w + fdst.y*fdst.w*(1 - fsrc.w))/fout.w; fout.z = (fsrc.z*fsrc.w + fdst.z*fdst.w*(1 - fsrc.w))/fout.w; } out = (Color){ (unsigned char)(fout.x*255.0f), (unsigned char)(fout.y*255.0f), (unsigned char)(fout.z*255.0f), (unsigned char)(fout.w*255.0f) }; } #endif return out; } // Get a Color struct from hexadecimal value Color GetColor(unsigned int hexValue) { Color color; color.r = (unsigned char)(hexValue >> 24) & 0xFF; color.g = (unsigned char)(hexValue >> 16) & 0xFF; color.b = (unsigned char)(hexValue >> 8) & 0xFF; color.a = (unsigned char)hexValue & 0xFF; return color; } // Get color from a pixel from certain format Color GetPixelColor(void *srcPtr, int format) { Color color = { 0 }; switch (format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: color = (Color){ ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[0], 255 }; break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: color = (Color){ ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[1] }; break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { color.r = (unsigned char)((((unsigned short *)srcPtr)[0] >> 11)*255/31); color.g = (unsigned char)(((((unsigned short *)srcPtr)[0] >> 5) & 0b0000000000111111)*255/63); color.b = (unsigned char)((((unsigned short *)srcPtr)[0] & 0b0000000000011111)*255/31); color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { color.r = (unsigned char)((((unsigned short *)srcPtr)[0] >> 11)*255/31); color.g = (unsigned char)(((((unsigned short *)srcPtr)[0] >> 6) & 0b0000000000011111)*255/31); color.b = (unsigned char)((((unsigned short *)srcPtr)[0] & 0b0000000000011111)*255/31); color.a = (((unsigned short *)srcPtr)[0] & 0b0000000000000001)? 255 : 0; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { color.r = (unsigned char)((((unsigned short *)srcPtr)[0] >> 12)*255/15); color.g = (unsigned char)(((((unsigned short *)srcPtr)[0] >> 8) & 0b0000000000001111)*255/15); color.b = (unsigned char)(((((unsigned short *)srcPtr)[0] >> 4) & 0b0000000000001111)*255/15); color.a = (unsigned char)((((unsigned short *)srcPtr)[0] & 0b0000000000001111)*255/15); } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: color = (Color){ ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[1], ((unsigned char *)srcPtr)[2], ((unsigned char *)srcPtr)[3] }; break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: color = (Color){ ((unsigned char *)srcPtr)[0], ((unsigned char *)srcPtr)[1], ((unsigned char *)srcPtr)[2], 255 }; break; case PIXELFORMAT_UNCOMPRESSED_R32: { // NOTE: Pixel normalized float value is converted to [0..255] color.r = (unsigned char)(((float *)srcPtr)[0]*255.0f); color.g = (unsigned char)(((float *)srcPtr)[0]*255.0f); color.b = (unsigned char)(((float *)srcPtr)[0]*255.0f); color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { // NOTE: Pixel normalized float value is converted to [0..255] color.r = (unsigned char)(((float *)srcPtr)[0]*255.0f); color.g = (unsigned char)(((float *)srcPtr)[1]*255.0f); color.b = (unsigned char)(((float *)srcPtr)[2]*255.0f); color.a = 255; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { // NOTE: Pixel normalized float value is converted to [0..255] color.r = (unsigned char)(((float *)srcPtr)[0]*255.0f); color.g = (unsigned char)(((float *)srcPtr)[1]*255.0f); color.b = (unsigned char)(((float *)srcPtr)[2]*255.0f); color.a = (unsigned char)(((float *)srcPtr)[3]*255.0f); } break; default: break; } return color; } // Set pixel color formatted into destination pointer void SetPixelColor(void *dstPtr, Color color, int format) { switch (format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { // NOTE: Calculate grayscale equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char gray = (unsigned char)((coln.x*0.299f + coln.y*0.587f + coln.z*0.114f)*255.0f); ((unsigned char *)dstPtr)[0] = gray; } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { // NOTE: Calculate grayscale equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char gray = (unsigned char)((coln.x*0.299f + coln.y*0.587f + coln.z*0.114f)*255.0f); ((unsigned char *)dstPtr)[0] = gray; ((unsigned char *)dstPtr)[1] = color.a; } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { // NOTE: Calculate R5G6B5 equivalent color Vector3 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f }; unsigned char r = (unsigned char)(round(coln.x*31.0f)); unsigned char g = (unsigned char)(round(coln.y*63.0f)); unsigned char b = (unsigned char)(round(coln.z*31.0f)); ((unsigned short *)dstPtr)[0] = (unsigned short)r << 11 | (unsigned short)g << 5 | (unsigned short)b; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { // NOTE: Calculate R5G5B5A1 equivalent color Vector4 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f, (float)color.a/255.0f }; unsigned char r = (unsigned char)(round(coln.x*31.0f)); unsigned char g = (unsigned char)(round(coln.y*31.0f)); unsigned char b = (unsigned char)(round(coln.z*31.0f)); unsigned char a = (coln.w > ((float)PIXELFORMAT_UNCOMPRESSED_R5G5B5A1_ALPHA_THRESHOLD/255.0f))? 1 : 0; ((unsigned short *)dstPtr)[0] = (unsigned short)r << 11 | (unsigned short)g << 6 | (unsigned short)b << 1 | (unsigned short)a; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { // NOTE: Calculate R5G5B5A1 equivalent color Vector4 coln = { (float)color.r/255.0f, (float)color.g/255.0f, (float)color.b/255.0f, (float)color.a/255.0f }; unsigned char r = (unsigned char)(round(coln.x*15.0f)); unsigned char g = (unsigned char)(round(coln.y*15.0f)); unsigned char b = (unsigned char)(round(coln.z*15.0f)); unsigned char a = (unsigned char)(round(coln.w*15.0f)); ((unsigned short *)dstPtr)[0] = (unsigned short)r << 12 | (unsigned short)g << 8 | (unsigned short)b << 4 | (unsigned short)a; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { ((unsigned char *)dstPtr)[0] = color.r; ((unsigned char *)dstPtr)[1] = color.g; ((unsigned char *)dstPtr)[2] = color.b; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { ((unsigned char *)dstPtr)[0] = color.r; ((unsigned char *)dstPtr)[1] = color.g; ((unsigned char *)dstPtr)[2] = color.b; ((unsigned char *)dstPtr)[3] = color.a; } break; default: break; } } // Get pixel data size in bytes for certain format // NOTE: Size can be requested for Image or Texture data int GetPixelDataSize(int width, int height, int format) { int dataSize = 0; // Size in bytes int bpp = 0; // Bits per pixel switch (format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: bpp = 8; break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: case PIXELFORMAT_UNCOMPRESSED_R5G6B5: case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: bpp = 16; break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: bpp = 32; break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: bpp = 24; break; case PIXELFORMAT_UNCOMPRESSED_R32: bpp = 32; break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: bpp = 32*3; break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: bpp = 32*4; break; case PIXELFORMAT_COMPRESSED_DXT1_RGB: case PIXELFORMAT_COMPRESSED_DXT1_RGBA: case PIXELFORMAT_COMPRESSED_ETC1_RGB: case PIXELFORMAT_COMPRESSED_ETC2_RGB: case PIXELFORMAT_COMPRESSED_PVRT_RGB: case PIXELFORMAT_COMPRESSED_PVRT_RGBA: bpp = 4; break; case PIXELFORMAT_COMPRESSED_DXT3_RGBA: case PIXELFORMAT_COMPRESSED_DXT5_RGBA: case PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA: case PIXELFORMAT_COMPRESSED_ASTC_4x4_RGBA: bpp = 8; break; case PIXELFORMAT_COMPRESSED_ASTC_8x8_RGBA: bpp = 2; break; default: break; } dataSize = width*height*bpp/8; // Total data size in bytes // Most compressed formats works on 4x4 blocks, // if texture is smaller, minimum dataSize is 8 or 16 if ((width < 4) && (height < 4)) { if ((format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) && (format < PIXELFORMAT_COMPRESSED_DXT3_RGBA)) dataSize = 8; else if ((format >= PIXELFORMAT_COMPRESSED_DXT3_RGBA) && (format < PIXELFORMAT_COMPRESSED_ASTC_8x8_RGBA)) dataSize = 16; } return dataSize; } //---------------------------------------------------------------------------------- // Module specific Functions Definition //---------------------------------------------------------------------------------- #if defined(SUPPORT_FILEFORMAT_DDS) // Loading DDS image data (compressed or uncompressed) static Image LoadDDS(const unsigned char *fileData, unsigned int fileSize) { unsigned char *fileDataPtr = (unsigned char *)fileData; // Required extension: // GL_EXT_texture_compression_s3tc // Supported tokens (defined by extensions) // GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0 // GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 0x83F1 // GL_COMPRESSED_RGBA_S3TC_DXT3_EXT 0x83F2 // GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3 #define FOURCC_DXT1 0x31545844 // Equivalent to "DXT1" in ASCII #define FOURCC_DXT3 0x33545844 // Equivalent to "DXT3" in ASCII #define FOURCC_DXT5 0x35545844 // Equivalent to "DXT5" in ASCII // DDS Pixel Format typedef struct { unsigned int size; unsigned int flags; unsigned int fourCC; unsigned int rgbBitCount; unsigned int rBitMask; unsigned int gBitMask; unsigned int bBitMask; unsigned int aBitMask; } DDSPixelFormat; // DDS Header (124 bytes) typedef struct { unsigned int size; unsigned int flags; unsigned int height; unsigned int width; unsigned int pitchOrLinearSize; unsigned int depth; unsigned int mipmapCount; unsigned int reserved1[11]; DDSPixelFormat ddspf; unsigned int caps; unsigned int caps2; unsigned int caps3; unsigned int caps4; unsigned int reserved2; } DDSHeader; Image image = { 0 }; if (fileDataPtr != NULL) { // Verify the type of file unsigned char *ddsHeaderId = fileDataPtr; fileDataPtr += 4; if ((ddsHeaderId[0] != 'D') || (ddsHeaderId[1] != 'D') || (ddsHeaderId[2] != 'S') || (ddsHeaderId[3] != ' ')) { TRACELOG(LOG_WARNING, "IMAGE: DDS file data not valid"); } else { DDSHeader *ddsHeader = (DDSHeader *)fileDataPtr; TRACELOGD("IMAGE: DDS file data info:"); TRACELOGD(" > Header size: %i", sizeof(DDSHeader)); TRACELOGD(" > Pixel format size: %i", ddsHeader->ddspf.size); TRACELOGD(" > Pixel format flags: 0x%x", ddsHeader->ddspf.flags); TRACELOGD(" > File format: 0x%x", ddsHeader->ddspf.fourCC); TRACELOGD(" > File bit count: 0x%x", ddsHeader->ddspf.rgbBitCount); fileDataPtr += sizeof(DDSHeader); // Skip header image.width = ddsHeader->width; image.height = ddsHeader->height; if (ddsHeader->mipmapCount == 0) image.mipmaps = 1; // Parameter not used else image.mipmaps = ddsHeader->mipmapCount; if (ddsHeader->ddspf.rgbBitCount == 16) // 16bit mode, no compressed { if (ddsHeader->ddspf.flags == 0x40) // no alpha channel { int dataSize = image.width*image.height*sizeof(unsigned short); image.data = (unsigned short *)RL_MALLOC(dataSize); memcpy(image.data, fileDataPtr, dataSize); image.format = PIXELFORMAT_UNCOMPRESSED_R5G6B5; } else if (ddsHeader->ddspf.flags == 0x41) // with alpha channel { if (ddsHeader->ddspf.aBitMask == 0x8000) // 1bit alpha { int dataSize = image.width*image.height*sizeof(unsigned short); image.data = (unsigned short *)RL_MALLOC(dataSize); memcpy(image.data, fileDataPtr, dataSize); unsigned char alpha = 0; // NOTE: Data comes as A1R5G5B5, it must be reordered to R5G5B5A1 for (int i = 0; i < image.width*image.height; i++) { alpha = ((unsigned short *)image.data)[i] >> 15; ((unsigned short *)image.data)[i] = ((unsigned short *)image.data)[i] << 1; ((unsigned short *)image.data)[i] += alpha; } image.format = PIXELFORMAT_UNCOMPRESSED_R5G5B5A1; } else if (ddsHeader->ddspf.aBitMask == 0xf000) // 4bit alpha { int dataSize = image.width*image.height*sizeof(unsigned short); image.data = (unsigned short *)RL_MALLOC(dataSize); memcpy(image.data, fileDataPtr, dataSize); unsigned char alpha = 0; // NOTE: Data comes as A4R4G4B4, it must be reordered R4G4B4A4 for (int i = 0; i < image.width*image.height; i++) { alpha = ((unsigned short *)image.data)[i] >> 12; ((unsigned short *)image.data)[i] = ((unsigned short *)image.data)[i] << 4; ((unsigned short *)image.data)[i] += alpha; } image.format = PIXELFORMAT_UNCOMPRESSED_R4G4B4A4; } } } else if (ddsHeader->ddspf.flags == 0x40 && ddsHeader->ddspf.rgbBitCount == 24) // DDS_RGB, no compressed { int dataSize = image.width*image.height*3*sizeof(unsigned char); image.data = (unsigned short *)RL_MALLOC(dataSize); memcpy(image.data, fileDataPtr, dataSize); image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8; } else if (ddsHeader->ddspf.flags == 0x41 && ddsHeader->ddspf.rgbBitCount == 32) // DDS_RGBA, no compressed { int dataSize = image.width*image.height*4*sizeof(unsigned char); image.data = (unsigned short *)RL_MALLOC(dataSize); memcpy(image.data, fileDataPtr, dataSize); unsigned char blue = 0; // NOTE: Data comes as A8R8G8B8, it must be reordered R8G8B8A8 (view next comment) // DirecX understand ARGB as a 32bit DWORD but the actual memory byte alignment is BGRA // So, we must realign B8G8R8A8 to R8G8B8A8 for (int i = 0; i < image.width*image.height*4; i += 4) { blue = ((unsigned char *)image.data)[i]; ((unsigned char *)image.data)[i] = ((unsigned char *)image.data)[i + 2]; ((unsigned char *)image.data)[i + 2] = blue; } image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; } else if (((ddsHeader->ddspf.flags == 0x04) || (ddsHeader->ddspf.flags == 0x05)) && (ddsHeader->ddspf.fourCC > 0)) // Compressed { int dataSize = 0; // Calculate data size, including all mipmaps if (ddsHeader->mipmapCount > 1) dataSize = ddsHeader->pitchOrLinearSize*2; else dataSize = ddsHeader->pitchOrLinearSize; image.data = (unsigned char *)RL_MALLOC(dataSize*sizeof(unsigned char)); memcpy(image.data, fileDataPtr, dataSize); switch (ddsHeader->ddspf.fourCC) { case FOURCC_DXT1: { if (ddsHeader->ddspf.flags == 0x04) image.format = PIXELFORMAT_COMPRESSED_DXT1_RGB; else image.format = PIXELFORMAT_COMPRESSED_DXT1_RGBA; } break; case FOURCC_DXT3: image.format = PIXELFORMAT_COMPRESSED_DXT3_RGBA; break; case FOURCC_DXT5: image.format = PIXELFORMAT_COMPRESSED_DXT5_RGBA; break; default: break; } } } } return image; } #endif #if defined(SUPPORT_FILEFORMAT_PKM) // Loading PKM image data (ETC1/ETC2 compression) // NOTE: KTX is the standard Khronos Group compression format (ETC1/ETC2, mipmaps) // PKM is a much simpler file format used mainly to contain a single ETC1/ETC2 compressed image (no mipmaps) static Image LoadPKM(const unsigned char *fileData, unsigned int fileSize) { unsigned char *fileDataPtr = (unsigned char *)fileData; // Required extensions: // GL_OES_compressed_ETC1_RGB8_texture (ETC1) (OpenGL ES 2.0) // GL_ARB_ES3_compatibility (ETC2/EAC) (OpenGL ES 3.0) // Supported tokens (defined by extensions) // GL_ETC1_RGB8_OES 0x8D64 // GL_COMPRESSED_RGB8_ETC2 0x9274 // GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 // PKM file (ETC1) Header (16 bytes) typedef struct { char id[4]; // "PKM " char version[2]; // "10" or "20" unsigned short format; // Data format (big-endian) (Check list below) unsigned short width; // Texture width (big-endian) (origWidth rounded to multiple of 4) unsigned short height; // Texture height (big-endian) (origHeight rounded to multiple of 4) unsigned short origWidth; // Original width (big-endian) unsigned short origHeight; // Original height (big-endian) } PKMHeader; // Formats list // version 10: format: 0=ETC1_RGB, [1=ETC1_RGBA, 2=ETC1_RGB_MIP, 3=ETC1_RGBA_MIP] (not used) // version 20: format: 0=ETC1_RGB, 1=ETC2_RGB, 2=ETC2_RGBA_OLD, 3=ETC2_RGBA, 4=ETC2_RGBA1, 5=ETC2_R, 6=ETC2_RG, 7=ETC2_SIGNED_R, 8=ETC2_SIGNED_R // NOTE: The extended width and height are the widths rounded up to a multiple of 4. // NOTE: ETC is always 4bit per pixel (64 bit for each 4x4 block of pixels) Image image = { 0 }; if (fileDataPtr != NULL) { PKMHeader *pkmHeader = (PKMHeader *)fileDataPtr; if ((pkmHeader->id[0] != 'P') || (pkmHeader->id[1] != 'K') || (pkmHeader->id[2] != 'M') || (pkmHeader->id[3] != ' ')) { TRACELOG(LOG_WARNING, "IMAGE: PKM file data not valid"); } else { fileDataPtr += sizeof(PKMHeader); // Skip header // NOTE: format, width and height come as big-endian, data must be swapped to little-endian pkmHeader->format = ((pkmHeader->format & 0x00FF) << 8) | ((pkmHeader->format & 0xFF00) >> 8); pkmHeader->width = ((pkmHeader->width & 0x00FF) << 8) | ((pkmHeader->width & 0xFF00) >> 8); pkmHeader->height = ((pkmHeader->height & 0x00FF) << 8) | ((pkmHeader->height & 0xFF00) >> 8); TRACELOGD("IMAGE: PKM file data info:"); TRACELOGD(" > Image width: %i", pkmHeader->width); TRACELOGD(" > Image height: %i", pkmHeader->height); TRACELOGD(" > Image format: %i", pkmHeader->format); image.width = pkmHeader->width; image.height = pkmHeader->height; image.mipmaps = 1; int bpp = 4; if (pkmHeader->format == 3) bpp = 8; int dataSize = image.width*image.height*bpp/8; // Total data size in bytes image.data = (unsigned char *)RL_MALLOC(dataSize*sizeof(unsigned char)); memcpy(image.data, fileDataPtr, dataSize); if (pkmHeader->format == 0) image.format = PIXELFORMAT_COMPRESSED_ETC1_RGB; else if (pkmHeader->format == 1) image.format = PIXELFORMAT_COMPRESSED_ETC2_RGB; else if (pkmHeader->format == 3) image.format = PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA; } } return image; } #endif #if defined(SUPPORT_FILEFORMAT_KTX) // Load KTX compressed image data (ETC1/ETC2 compression) // TODO: Review KTX loading, many things changed! static Image LoadKTX(const unsigned char *fileData, unsigned int fileSize) { unsigned char *fileDataPtr = (unsigned char *)fileData; // Required extensions: // GL_OES_compressed_ETC1_RGB8_texture (ETC1) // GL_ARB_ES3_compatibility (ETC2/EAC) // Supported tokens (defined by extensions) // GL_ETC1_RGB8_OES 0x8D64 // GL_COMPRESSED_RGB8_ETC2 0x9274 // GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 // KTX file Header (64 bytes) // v1.1 - https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ // v2.0 - http://github.khronos.org/KTX-Specification/ // TODO: Support KTX 2.2 specs! typedef struct { char id[12]; // Identifier: "«KTX 11»\r\n\x1A\n" unsigned int endianness; // Little endian: 0x01 0x02 0x03 0x04 unsigned int glType; // For compressed textures, glType must equal 0 unsigned int glTypeSize; // For compressed texture data, usually 1 unsigned int glFormat; // For compressed textures is 0 unsigned int glInternalFormat; // Compressed internal format unsigned int glBaseInternalFormat; // Same as glFormat (RGB, RGBA, ALPHA...) unsigned int width; // Texture image width in pixels unsigned int height; // Texture image height in pixels unsigned int depth; // For 2D textures is 0 unsigned int elements; // Number of array elements, usually 0 unsigned int faces; // Cubemap faces, for no-cubemap = 1 unsigned int mipmapLevels; // Non-mipmapped textures = 1 unsigned int keyValueDataSize; // Used to encode any arbitrary data... } KTXHeader; // NOTE: Before start of every mipmap data block, we have: unsigned int dataSize Image image = { 0 }; if (fileDataPtr != NULL) { KTXHeader *ktxHeader = (KTXHeader *)fileDataPtr; if ((ktxHeader->id[1] != 'K') || (ktxHeader->id[2] != 'T') || (ktxHeader->id[3] != 'X') || (ktxHeader->id[4] != ' ') || (ktxHeader->id[5] != '1') || (ktxHeader->id[6] != '1')) { TRACELOG(LOG_WARNING, "IMAGE: KTX file data not valid"); } else { fileDataPtr += sizeof(KTXHeader); // Move file data pointer image.width = ktxHeader->width; image.height = ktxHeader->height; image.mipmaps = ktxHeader->mipmapLevels; TRACELOGD("IMAGE: KTX file data info:"); TRACELOGD(" > Image width: %i", ktxHeader->width); TRACELOGD(" > Image height: %i", ktxHeader->height); TRACELOGD(" > Image format: 0x%x", ktxHeader->glInternalFormat); fileDataPtr += ktxHeader->keyValueDataSize; // Skip value data size int dataSize = ((int *)fileDataPtr)[0]; fileDataPtr += sizeof(int); image.data = (unsigned char *)RL_MALLOC(dataSize*sizeof(unsigned char)); memcpy(image.data, fileDataPtr, dataSize); if (ktxHeader->glInternalFormat == 0x8D64) image.format = PIXELFORMAT_COMPRESSED_ETC1_RGB; else if (ktxHeader->glInternalFormat == 0x9274) image.format = PIXELFORMAT_COMPRESSED_ETC2_RGB; else if (ktxHeader->glInternalFormat == 0x9278) image.format = PIXELFORMAT_COMPRESSED_ETC2_EAC_RGBA; // TODO: Support uncompressed data formats? Right now it returns format = 0! } } return image; } // Save image data as KTX file // NOTE: By default KTX 1.1 spec is used, 2.0 is still on draft (01Oct2018) // TODO: Review KTX saving, many things changed! static int SaveKTX(Image image, const char *fileName) { // KTX file Header (64 bytes) // v1.1 - https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ // v2.0 - http://github.khronos.org/KTX-Specification/ - Final specs by 2021-04-18 typedef struct { char id[12]; // Identifier: "«KTX 11»\r\n\x1A\n" // KTX 2.0: "«KTX 22»\r\n\x1A\n" unsigned int endianness; // Little endian: 0x01 0x02 0x03 0x04 unsigned int glType; // For compressed textures, glType must equal 0 unsigned int glTypeSize; // For compressed texture data, usually 1 unsigned int glFormat; // For compressed textures is 0 unsigned int glInternalFormat; // Compressed internal format unsigned int glBaseInternalFormat; // Same as glFormat (RGB, RGBA, ALPHA...) // KTX 2.0: UInt32 vkFormat unsigned int width; // Texture image width in pixels unsigned int height; // Texture image height in pixels unsigned int depth; // For 2D textures is 0 unsigned int elements; // Number of array elements, usually 0 unsigned int faces; // Cubemap faces, for no-cubemap = 1 unsigned int mipmapLevels; // Non-mipmapped textures = 1 unsigned int keyValueDataSize; // Used to encode any arbitrary data... // KTX 2.0: UInt32 levelOrder - ordering of the mipmap levels, usually 0 // KTX 2.0: UInt32 supercompressionScheme - 0 (None), 1 (Crunch CRN), 2 (Zlib DEFLATE)... // KTX 2.0 defines additional header elements... } KTXHeader; // Calculate file dataSize required int dataSize = sizeof(KTXHeader); for (int i = 0, width = image.width, height = image.height; i < image.mipmaps; i++) { dataSize += GetPixelDataSize(width, height, image.format); width /= 2; height /= 2; } unsigned char *fileData = RL_CALLOC(dataSize, 1); unsigned char *fileDataPtr = fileData; KTXHeader ktxHeader = { 0 }; // KTX identifier (v1.1) //unsigned char id[12] = { '«', 'K', 'T', 'X', ' ', '1', '1', '»', '\r', '\n', '\x1A', '\n' }; //unsigned char id[12] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; const char ktxIdentifier[12] = { 0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, '\r', '\n', 0x1A, '\n' }; // Get the image header memcpy(ktxHeader.id, ktxIdentifier, 12); // KTX 1.1 signature ktxHeader.endianness = 0; ktxHeader.glType = 0; // Obtained from image.format ktxHeader.glTypeSize = 1; ktxHeader.glFormat = 0; // Obtained from image.format ktxHeader.glInternalFormat = 0; // Obtained from image.format ktxHeader.glBaseInternalFormat = 0; ktxHeader.width = image.width; ktxHeader.height = image.height; ktxHeader.depth = 0; ktxHeader.elements = 0; ktxHeader.faces = 1; ktxHeader.mipmapLevels = image.mipmaps; // If it was 0, it means mipmaps should be generated on loading (not for compressed formats) ktxHeader.keyValueDataSize = 0; // No extra data after the header rlGetGlTextureFormats(image.format, &ktxHeader.glInternalFormat, &ktxHeader.glFormat, &ktxHeader.glType); // rlgl module function ktxHeader.glBaseInternalFormat = ktxHeader.glFormat; // KTX 1.1 only // NOTE: We can save into a .ktx all PixelFormats supported by raylib, including compressed formats like DXT, ETC or ASTC if (ktxHeader.glFormat == -1) TRACELOG(LOG_WARNING, "IMAGE: GL format not supported for KTX export (%i)", ktxHeader.glFormat); else { memcpy(fileDataPtr, &ktxHeader, sizeof(KTXHeader)); fileDataPtr += sizeof(KTXHeader); int width = image.width; int height = image.height; int dataOffset = 0; // Save all mipmaps data for (int i = 0; i < image.mipmaps; i++) { unsigned int dataSize = GetPixelDataSize(width, height, image.format); memcpy(fileDataPtr, &dataSize, sizeof(unsigned int)); memcpy(fileDataPtr + 4, (unsigned char *)image.data + dataOffset, dataSize); width /= 2; height /= 2; dataOffset += dataSize; fileDataPtr += (4 + dataSize); } } int success = SaveFileData(fileName, fileData, dataSize); RL_FREE(fileData); // Free file data buffer // If all data has been written correctly to file, success = 1 return success; } #endif #if defined(SUPPORT_FILEFORMAT_PVR) // Loading PVR image data (uncompressed or PVRT compression) // NOTE: PVR v2 not supported, use PVR v3 instead static Image LoadPVR(const unsigned char *fileData, unsigned int fileSize) { unsigned char *fileDataPtr = (unsigned char *)fileData; // Required extension: // GL_IMG_texture_compression_pvrtc // Supported tokens (defined by extensions) // GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG 0x8C00 // GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG 0x8C02 #if 0 // Not used... // PVR file v2 Header (52 bytes) typedef struct { unsigned int headerLength; unsigned int height; unsigned int width; unsigned int numMipmaps; unsigned int flags; unsigned int dataLength; unsigned int bpp; unsigned int bitmaskRed; unsigned int bitmaskGreen; unsigned int bitmaskBlue; unsigned int bitmaskAlpha; unsigned int pvrTag; unsigned int numSurfs; } PVRHeaderV2; #endif // PVR file v3 Header (52 bytes) // NOTE: After it could be metadata (15 bytes?) typedef struct { char id[4]; unsigned int flags; unsigned char channels[4]; // pixelFormat high part unsigned char channelDepth[4]; // pixelFormat low part unsigned int colourSpace; unsigned int channelType; unsigned int height; unsigned int width; unsigned int depth; unsigned int numSurfaces; unsigned int numFaces; unsigned int numMipmaps; unsigned int metaDataSize; } PVRHeaderV3; #if 0 // Not used... // Metadata (usually 15 bytes) typedef struct { unsigned int devFOURCC; unsigned int key; unsigned int dataSize; // Not used? unsigned char *data; // Not used? } PVRMetadata; #endif Image image = { 0 }; if (fileDataPtr != NULL) { // Check PVR image version unsigned char pvrVersion = fileDataPtr[0]; // Load different PVR data formats if (pvrVersion == 0x50) { PVRHeaderV3 *pvrHeader = (PVRHeaderV3 *)fileDataPtr; if ((pvrHeader->id[0] != 'P') || (pvrHeader->id[1] != 'V') || (pvrHeader->id[2] != 'R') || (pvrHeader->id[3] != 3)) { TRACELOG(LOG_WARNING, "IMAGE: PVR file data not valid"); } else { fileDataPtr += sizeof(PVRHeaderV3); // Skip header image.width = pvrHeader->width; image.height = pvrHeader->height; image.mipmaps = pvrHeader->numMipmaps; // Check data format if (((pvrHeader->channels[0] == 'l') && (pvrHeader->channels[1] == 0)) && (pvrHeader->channelDepth[0] == 8)) image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE; else if (((pvrHeader->channels[0] == 'l') && (pvrHeader->channels[1] == 'a')) && ((pvrHeader->channelDepth[0] == 8) && (pvrHeader->channelDepth[1] == 8))) image.format = PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA; else if ((pvrHeader->channels[0] == 'r') && (pvrHeader->channels[1] == 'g') && (pvrHeader->channels[2] == 'b')) { if (pvrHeader->channels[3] == 'a') { if ((pvrHeader->channelDepth[0] == 5) && (pvrHeader->channelDepth[1] == 5) && (pvrHeader->channelDepth[2] == 5) && (pvrHeader->channelDepth[3] == 1)) image.format = PIXELFORMAT_UNCOMPRESSED_R5G5B5A1; else if ((pvrHeader->channelDepth[0] == 4) && (pvrHeader->channelDepth[1] == 4) && (pvrHeader->channelDepth[2] == 4) && (pvrHeader->channelDepth[3] == 4)) image.format = PIXELFORMAT_UNCOMPRESSED_R4G4B4A4; else if ((pvrHeader->channelDepth[0] == 8) && (pvrHeader->channelDepth[1] == 8) && (pvrHeader->channelDepth[2] == 8) && (pvrHeader->channelDepth[3] == 8)) image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; } else if (pvrHeader->channels[3] == 0) { if ((pvrHeader->channelDepth[0] == 5) && (pvrHeader->channelDepth[1] == 6) && (pvrHeader->channelDepth[2] == 5)) image.format = PIXELFORMAT_UNCOMPRESSED_R5G6B5; else if ((pvrHeader->channelDepth[0] == 8) && (pvrHeader->channelDepth[1] == 8) && (pvrHeader->channelDepth[2] == 8)) image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8; } } else if (pvrHeader->channels[0] == 2) image.format = PIXELFORMAT_COMPRESSED_PVRT_RGB; else if (pvrHeader->channels[0] == 3) image.format = PIXELFORMAT_COMPRESSED_PVRT_RGBA; fileDataPtr += pvrHeader->metaDataSize; // Skip meta data header // Calculate data size (depends on format) int bpp = 0; switch (image.format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: bpp = 8; break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: case PIXELFORMAT_UNCOMPRESSED_R5G6B5: case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: bpp = 16; break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: bpp = 32; break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: bpp = 24; break; case PIXELFORMAT_COMPRESSED_PVRT_RGB: case PIXELFORMAT_COMPRESSED_PVRT_RGBA: bpp = 4; break; default: break; } int dataSize = image.width*image.height*bpp/8; // Total data size in bytes image.data = (unsigned char *)RL_MALLOC(dataSize*sizeof(unsigned char)); memcpy(image.data, fileDataPtr, dataSize); } } else if (pvrVersion == 52) TRACELOG(LOG_INFO, "IMAGE: PVRv2 format not supported, update your files to PVRv3"); } return image; } #endif #if defined(SUPPORT_FILEFORMAT_ASTC) // Load ASTC compressed image data (ASTC compression) static Image LoadASTC(const unsigned char *fileData, unsigned int fileSize) { unsigned char *fileDataPtr = (unsigned char *)fileData; // Required extensions: // GL_KHR_texture_compression_astc_hdr // GL_KHR_texture_compression_astc_ldr // Supported tokens (defined by extensions) // GL_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93b0 // GL_COMPRESSED_RGBA_ASTC_8x8_KHR 0x93b7 // ASTC file Header (16 bytes) typedef struct { unsigned char id[4]; // Signature: 0x13 0xAB 0xA1 0x5C unsigned char blockX; // Block X dimensions unsigned char blockY; // Block Y dimensions unsigned char blockZ; // Block Z dimensions (1 for 2D images) unsigned char width[3]; // Image width in pixels (24bit value) unsigned char height[3]; // Image height in pixels (24bit value) unsigned char length[3]; // Image Z-size (1 for 2D images) } ASTCHeader; Image image = { 0 }; if (fileDataPtr != NULL) { ASTCHeader *astcHeader = (ASTCHeader *)fileDataPtr; if ((astcHeader->id[3] != 0x5c) || (astcHeader->id[2] != 0xa1) || (astcHeader->id[1] != 0xab) || (astcHeader->id[0] != 0x13)) { TRACELOG(LOG_WARNING, "IMAGE: ASTC file data not valid"); } else { fileDataPtr += sizeof(ASTCHeader); // Skip header // NOTE: Assuming Little Endian (could it be wrong?) image.width = 0x00000000 | ((int)astcHeader->width[2] << 16) | ((int)astcHeader->width[1] << 8) | ((int)astcHeader->width[0]); image.height = 0x00000000 | ((int)astcHeader->height[2] << 16) | ((int)astcHeader->height[1] << 8) | ((int)astcHeader->height[0]); TRACELOGD("IMAGE: ASTC file data info:"); TRACELOGD(" > Image width: %i", image.width); TRACELOGD(" > Image height: %i", image.height); TRACELOGD(" > Image blocks: %ix%i", astcHeader->blockX, astcHeader->blockY); image.mipmaps = 1; // NOTE: ASTC format only contains one mipmap level // NOTE: Each block is always stored in 128bit so we can calculate the bpp int bpp = 128/(astcHeader->blockX*astcHeader->blockY); // NOTE: Currently we only support 2 blocks configurations: 4x4 and 8x8 if ((bpp == 8) || (bpp == 2)) { int dataSize = image.width*image.height*bpp/8; // Data size in bytes image.data = (unsigned char *)RL_MALLOC(dataSize*sizeof(unsigned char)); memcpy(image.data, fileDataPtr, dataSize); if (bpp == 8) image.format = PIXELFORMAT_COMPRESSED_ASTC_4x4_RGBA; else if (bpp == 2) image.format = PIXELFORMAT_COMPRESSED_ASTC_8x8_RGBA; } else TRACELOG(LOG_WARNING, "IMAGE: ASTC block size configuration not supported"); } } return image; } #endif // Get pixel data from image as Vector4 array (float normalized) static Vector4 *LoadImageDataNormalized(Image image) { Vector4 *pixels = (Vector4 *)RL_MALLOC(image.width*image.height*sizeof(Vector4)); if (image.format >= PIXELFORMAT_COMPRESSED_DXT1_RGB) TRACELOG(LOG_WARNING, "IMAGE: Pixel data retrieval not supported for compressed image formats"); else { for (int i = 0, k = 0; i < image.width*image.height; i++) { switch (image.format) { case PIXELFORMAT_UNCOMPRESSED_GRAYSCALE: { pixels[i].x = (float)((unsigned char *)image.data)[i]/255.0f; pixels[i].y = (float)((unsigned char *)image.data)[i]/255.0f; pixels[i].z = (float)((unsigned char *)image.data)[i]/255.0f; pixels[i].w = 1.0f; } break; case PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA: { pixels[i].x = (float)((unsigned char *)image.data)[k]/255.0f; pixels[i].y = (float)((unsigned char *)image.data)[k]/255.0f; pixels[i].z = (float)((unsigned char *)image.data)[k]/255.0f; pixels[i].w = (float)((unsigned char *)image.data)[k + 1]/255.0f; k += 2; } break; case PIXELFORMAT_UNCOMPRESSED_R5G5B5A1: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].x = (float)((pixel & 0b1111100000000000) >> 11)*(1.0f/31); pixels[i].y = (float)((pixel & 0b0000011111000000) >> 6)*(1.0f/31); pixels[i].z = (float)((pixel & 0b0000000000111110) >> 1)*(1.0f/31); pixels[i].w = ((pixel & 0b0000000000000001) == 0)? 0.0f : 1.0f; } break; case PIXELFORMAT_UNCOMPRESSED_R5G6B5: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].x = (float)((pixel & 0b1111100000000000) >> 11)*(1.0f/31); pixels[i].y = (float)((pixel & 0b0000011111100000) >> 5)*(1.0f/63); pixels[i].z = (float)(pixel & 0b0000000000011111)*(1.0f/31); pixels[i].w = 1.0f; } break; case PIXELFORMAT_UNCOMPRESSED_R4G4B4A4: { unsigned short pixel = ((unsigned short *)image.data)[i]; pixels[i].x = (float)((pixel & 0b1111000000000000) >> 12)*(1.0f/15); pixels[i].y = (float)((pixel & 0b0000111100000000) >> 8)*(1.0f/15); pixels[i].z = (float)((pixel & 0b0000000011110000) >> 4)*(1.0f/15); pixels[i].w = (float)(pixel & 0b0000000000001111)*(1.0f/15); } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8A8: { pixels[i].x = (float)((unsigned char *)image.data)[k]/255.0f; pixels[i].y = (float)((unsigned char *)image.data)[k + 1]/255.0f; pixels[i].z = (float)((unsigned char *)image.data)[k + 2]/255.0f; pixels[i].w = (float)((unsigned char *)image.data)[k + 3]/255.0f; k += 4; } break; case PIXELFORMAT_UNCOMPRESSED_R8G8B8: { pixels[i].x = (float)((unsigned char *)image.data)[k]/255.0f; pixels[i].y = (float)((unsigned char *)image.data)[k + 1]/255.0f; pixels[i].z = (float)((unsigned char *)image.data)[k + 2]/255.0f; pixels[i].w = 1.0f; k += 3; } break; case PIXELFORMAT_UNCOMPRESSED_R32: { pixels[i].x = ((float *)image.data)[k]; pixels[i].y = 0.0f; pixels[i].z = 0.0f; pixels[i].w = 1.0f; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32: { pixels[i].x = ((float *)image.data)[k]; pixels[i].y = ((float *)image.data)[k + 1]; pixels[i].z = ((float *)image.data)[k + 2]; pixels[i].w = 1.0f; k += 3; } break; case PIXELFORMAT_UNCOMPRESSED_R32G32B32A32: { pixels[i].x = ((float *)image.data)[k]; pixels[i].y = ((float *)image.data)[k + 1]; pixels[i].z = ((float *)image.data)[k + 2]; pixels[i].w = ((float *)image.data)[k + 3]; k += 4; } default: break; } } } return pixels; } #endif // SUPPORT_MODULE_RTEXTURES