#include "Rendering/Texture.h" #include "Common/String.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #define GET_CHANNEL_DATA(pixel, channel, channel_count, default, max) (channel < channel_count ? pixel[channel] : default) / max bool texture_collection_init(uint16_t size, texture_collection_t* textures) { texture_collection_t temp = {0}; temp.buffer = (texture_asset_t*)malloc(size * sizeof(texture_asset_t)); if (temp.buffer == NULL) { return false; } temp.size = size; temp.count = 0; *textures = temp; return true; } void texture_collection_resize(texture_collection_t* textures, uint16_t size) { if (size == INVALID_TEXTURE_ID) { size = INVALID_TEXTURE_ID - 1; } if (size == textures->size) { return; } texture_asset_t* temp = (texture_asset_t*)realloc(textures->buffer, size * sizeof(texture_asset_t)); if (temp != NULL) { textures->buffer = temp; textures->size = size; } } void texture_collection_free(texture_collection_t* textures) { if (textures == NULL) { return; } for (uint16_t i = 0; i < textures->count; i++) { texture_free(&textures->buffer[i].texture); char* full_name = textures->buffer[i].full_name; if (full_name != NULL) { free(full_name); } } free(textures->buffer); textures->buffer = NULL; } static inline void read_pixel_raw(const char* data, uint32_t x, uint32_t y, uint32_t width, uint8_t channel_count, stride_t stride, char* out_pixel_data) { size_t pixel_offset = (size_t)(y * width + x) * channel_count * stride; memcpy(out_pixel_data, data + pixel_offset, (size_t)channel_count * stride); } static inline void write_pixel_raw(char* data, uint32_t x, uint32_t y, uint32_t width, uint8_t channel_count, stride_t stride, const char* in_pixel_data) { size_t pixel_offset = (size_t)(y * width + x) * channel_count * stride; memcpy(data + pixel_offset, in_pixel_data, (size_t)channel_count * stride); } static void average_pixels_box(const char* current_data, uint32_t current_width, uint32_t current_height, uint32_t src_x, uint32_t src_y, uint8_t channel_count, stride_t stride, char* out_averaged_pixel) { size_t pixel_byte_size = (size_t)channel_count * stride; float pixel_count = 0.0f; #if defined(__clang__) || defined(__GNUC__) char pixel_data[pixel_byte_size]; // Buffer to read individual pixel data // Use a float buffer to accumulate the sum for each channel // This allows us to sum values from different strides by converting them to float float sum_float[channel_count]; memset(sum_float, 0, sizeof(float) * channel_count); #else char* pixel_data = (char*)malloc(pixel_byte_size); // Buffer to read individual pixel data if (pixel_data == NULL) { return; } float* sum_float = (float*)calloc(channel_count, sizeof(float)); if (sum_float == NULL) { free(pixel_data); return; } #endif // Loop through the 2x2 block in the current level for (int dy = 0; dy < 2; ++dy) { for (int dx = 0; dx < 2; ++dx) { uint32_t current_x = src_x + dx; uint32_t current_y = src_y + dy; // Check if the pixel is within the bounds of the current level if (current_x < current_width && current_y < current_height) { // Read the raw pixel data read_pixel_raw(current_data, current_x, current_y, current_width, channel_count, stride, pixel_data); // Sum the pixel data channel by channel, converting to float for summation for (uint8_t c = 0; c < channel_count; c++) { switch (stride) { case UINT_8: sum_float[c] += (float)(((uint8_t*)pixel_data)[c]); break; case UINT_16: sum_float[c] += (float)(((uint16_t*)pixel_data)[c]); break; case FLOAT_32: sum_float[c] += ((float*)pixel_data)[c]; break; default: break; } } pixel_count += 1.0f; } } } // Divide the sum by the pixel count to get the average for each channel if (pixel_count > 0.0f) { // Convert the averaged float values back to the original stride type and write to the output buffer for (uint8_t c = 0; c < channel_count; c++) { float average_value = sum_float[c] / pixel_count; switch (stride) { case UINT_8: ((uint8_t*)out_averaged_pixel)[c] = (uint8_t)glm_clamp(average_value, 0.0f, 255.0f); break; case UINT_16: ((uint16_t*)out_averaged_pixel)[c] = (uint16_t)glm_clamp(average_value, 0.0f, 65535.0f); break; case FLOAT_32: ((float*)out_averaged_pixel)[c] = average_value; break; default: break; } } } else { // This case should ideally not happen if current_width or current_height > 1, // but as a safeguard, zero out the output buffer. memset(out_averaged_pixel, 0, pixel_byte_size); } #if !defined(__clang__) && !defined(__GNUC__) free(pixel_data); free(sum_float); #endif } static void generate_mipmap(char* raw_data, mipmap_t* texture_data, uint32_t width, uint32_t height, uint8_t channel_count, uint8_t max_level, stride_t stride) { // Store the base level (Level 0) texture_data[0] = (mipmap_t) { .width = width, .height = height, .data = raw_data, }; char* current_data = raw_data; uint32_t current_width = width; uint32_t current_height = height; int level = 1; uint32_t pixel_byte_size = channel_count * stride; #if defined(__clang__) || defined(__GNUC__) char averaged_pixel_buffer[pixel_byte_size]; #else char* averaged_pixel_buffer = (char*)malloc(pixel_byte_size); if (averaged_pixel_buffer == NULL) { return; } #endif // Continue generating levels as long as at least one dimension is greater than 1 while ((current_width > 1 || current_height > 1) && level <= max_level) { uint32_t next_width = max(1u, current_width / 2); uint32_t next_height = max(1u, current_height / 2); size_t next_level_size = (size_t)next_width * next_height * channel_count * stride; char* next_data = (char*)malloc(next_level_size); if (next_data == NULL) { break; } // Iterate through each pixel in the NEXT mipmap level for (uint32_t y = 0; y < next_height; ++y) { for (uint32_t x = 0; x < next_width; ++x) { // Calculate the starting coordinates (top-left corner) of the 2x2 block in the CURRENT level uint32_t src_x = x * 2; uint32_t src_y = y * 2; // Average the pixels in the 2x2 block from the CURRENT level using Box filter average_pixels_box(current_data, current_width, current_height, src_x, src_y, channel_count, stride, averaged_pixel_buffer); // Write the averaged pixel value to the corresponding location in the NEXT level write_pixel_raw(next_data, x, y, next_width, channel_count, stride, averaged_pixel_buffer); } } texture_data[level] = (mipmap_t) { .width = next_width, .height = next_height, .data = next_data, }; // Update for the next iteration current_data = next_data; current_width = next_width; current_height = next_height; level++; } #if !defined(__clang__) && !defined(__GNUC__) free(averaged_pixel_buffer); #endif } texture_handle_t texture_load(const char* filename, bool srgb, bool mipmap, stride_t stride, texture_collection_t* textures) { // TODO: This hurts performance, consider using a hash map or similar structure for faster lookups // for (uint16_t i = 0; i < textures->count; i++) // { // if (strcmp(textures->buffer[i].full_name, filename) == 0) // { // return (texture_entity_t){.id = i}; // } // } int width, height, channels; char* raw_data = NULL; switch (stride) { case UINT_8: raw_data = (char*)stbi_load(filename, &width, &height, &channels, 0); break; case UINT_16: raw_data = (char*)stbi_load_16(filename, &width, &height, &channels, 0); break; case FLOAT_32: raw_data = (char*)stbi_loadf(filename, &width, &height, &channels, 0); break; } if (raw_data == NULL) { return invalid_texture_handle(); } uint8_t max_mip_level = mipmap ? (uint8_t)log2f(fmaxf((float)width, (float)height)) : 0; mipmap_t* temp_texture_data = (mipmap_t*)calloc((size_t)max_mip_level + 1, sizeof(mipmap_t)); if (temp_texture_data == NULL) { stbi_image_free(raw_data); return invalid_texture_handle(); } generate_mipmap(raw_data, temp_texture_data, (uint32_t)width, (uint32_t)height, (uint8_t)channels, max_mip_level, stride); texture_t texture = {0}; texture.texel_size = (vec2s){1.0f / (float)width, 1.0f / (float)height}; texture.width = (uint32_t)width; texture.height = (uint32_t)height; texture.channel_count = (uint8_t)channels; texture.max_mip = max_mip_level; texture.stride = stride; texture.data = temp_texture_data; texture.wrap_mode = WM_REPEAT; texture.filter_mode = FM_LINEAR; if (textures->count >= textures->size) { texture_collection_resize(textures, textures->size * 2); } texture_handle_t entity = {.id = textures->count}; textures->buffer[textures->count] = (texture_asset_t){.full_name = string_copy(filename), .texture = texture}; textures->count++; return entity; } static inline void warp_uv(wrap_mode_t mode, vec2s* uv) { switch (mode) { case WM_REPEAT: uv->x = fmodf(fabsf(uv->x), 1.0f); uv->y = fmodf(fabsf(uv->y), 1.0f); break; case WM_CLAMP: *uv = glms_vec2_clamp(*uv, 0.0f, 1.0f); break; } } static vec4s get_pixel_data_from_buffer(const char* data, uint32_t x, uint32_t y, uint32_t width, uint8_t channel_count, stride_t stride) { size_t pixel_start_offset = (size_t)(y * width + x) * channel_count * stride; vec4s out = {0.0f, 0.0f, 0.0f, 1.0f}; for (int c = 0; c < channel_count && c < 4; c++) { float value = 0.0f; size_t channel_offset = pixel_start_offset + (size_t)c * stride; if (channel_offset >= (size_t)y * width * channel_count * stride + (size_t)width * channel_count * stride) { continue; } switch (stride) { case UINT_8: value = (float)(((uint8_t*)data)[channel_offset]) / 255.0f; break; case UINT_16: value = (float)(*((uint16_t*)(data + channel_offset))) / 65535.0f; break; case FLOAT_32: value = *((float*)(data + channel_offset)); break; default: value = (c == 3) ? 1.0f : 0.0f; break; } out.raw[c] = value; } return out; } vec4s texture_get_pixel(const texture_t* texture, vec2s uv, uint8_t lod) { uint8_t mip_level = (uint8_t)glm_clamp(lod, 0, texture->max_mip); const mipmap_t* mipmap = &texture->data[mip_level]; if (mipmap->data == NULL) { return (vec4s){0.0f, 0.0f, 0.0f, 1.0f}; } uint32_t x = (uint32_t)floorf(uv.x * (mipmap->width - 1)); uint32_t y = (uint32_t)floorf(uv.y * (mipmap->height - 1)); x = x < mipmap->width ? x : mipmap->width - 1; y = y < mipmap->height ? y : mipmap->height - 1; return get_pixel_data_from_buffer(mipmap->data, x, y, mipmap->width, texture->channel_count, texture->stride); } // Calculate LOD based on Ray Cones float texture_get_sample_lod(const texture_t* texture, const texture_sample_context_t* sample_context) { // 1. Calculate the ray footprint on the surface // If we hit the surface at an angle, the footprint elongates. float cos_theta = fabsf(glms_vec3_dot(sample_context->normal, sample_context->view_direction)); float surface_width = sample_context->ray_width / fmaxf(cos_theta, 0.001f); // Project width onto surface // 2. Estimate UV density (How much UV changes per meter of surface) // This is an approximation. A more accurate way uses Triangle derivatives (Ray Differentials). // For a triangle, we can approximate the scale: float edge1_len = glms_vec3_norm(sample_context->edge1); float edge2_len = glms_vec3_norm(sample_context->edge2); float uv_area = fabsf((sample_context->uv1.x * sample_context->uv2.y) - (sample_context->uv1.y * sample_context->uv2.x)); // Approximation of UV area float geo_area = glms_vec3_norm(glms_vec3_cross(sample_context->edge1, sample_context->edge2)); // Ratio of Texture Area to Geometric Area float uv_density = sqrtf(uv_area / geo_area); // 3. Calculate texture footprint // How many texels does our ray cover? float texels_covered = surface_width * uv_density * fmaxf(texture->width, texture->height); // 4. Convert to LOD // LOD 0 = 1 texel. LOD 1 = 2 texels. LOD 2 = 4 texels. // log2(texels_covered) gives the mip level. return log2f(texels_covered) * 0.5f; } static vec4s nearest_filter(const texture_t* texture, vec2s uv, uint8_t lod) { return texture_get_pixel(texture, uv, lod); } static vec4s linear_filter(const texture_t* texture, vec2s uv, uint8_t lod) { uint8_t mip_level = (uint8_t)glm_clamp((float)lod, 0.0f, (float)texture->max_mip); const mipmap_t* mipmap = &texture->data[mip_level]; if (mipmap->data == NULL) { return (vec4s){0.0f, 0.0f, 0.0f, 1.0f}; } float x = uv.x * (float)(mipmap->width - 1); float y = uv.y * (float)(mipmap->height - 1); uint32_t x0 = (uint32_t)floorf(x); uint32_t y0 = (uint32_t)floorf(y); uint32_t x1 = x0 + 1; uint32_t y1 = y0 + 1; float sx = x - (float)x0; float sy = y - (float)y0; x0 = (uint32_t)glm_clamp((float)x0, 0.0f, (float)mipmap->width - 1.0f); x1 = (uint32_t)glm_clamp((float)x1, 0.0f, (float)mipmap->width - 1.0f); y0 = (uint32_t)glm_clamp((float)y0, 0.0f, (float)mipmap->height - 1.0f); y1 = (uint32_t)glm_clamp((float)y1, 0.0f, (float)mipmap->height - 1.0f); // Get the pixel values for the four corners of the 2x2 block vec4s c00 = get_pixel_data_from_buffer(mipmap->data, x0, y0, mipmap->width, texture->channel_count, texture->stride); vec4s c10 = get_pixel_data_from_buffer(mipmap->data, x1, y0, mipmap->width, texture->channel_count, texture->stride); vec4s c01 = get_pixel_data_from_buffer(mipmap->data, x0, y1, mipmap->width, texture->channel_count, texture->stride); vec4s c11 = get_pixel_data_from_buffer(mipmap->data, x1, y1, mipmap->width, texture->channel_count, texture->stride); vec4s c0 = glms_vec4_lerp(c00, c10, sx); // Interpolate along x for the top row vec4s c1 = glms_vec4_lerp(c01, c11, sx); // Interpolate along x for the bottom row vec4s result = glms_vec4_lerp(c0, c1, sy); // Interpolate along y return result; } static inline vec4s filter_texture(const texture_t* texture, vec2s uv, float lod) { switch (texture->filter_mode) { case FM_NEAREST: return nearest_filter(texture, uv, (uint8_t)lod); case FM_LINEAR: return linear_filter(texture, uv, (uint8_t)lod); default: return (vec4s){0.0f, 0.0f, 0.0f, 1.0f}; } } vec4s texture_sample(const texture_t* texture, const texture_sample_context_t* sample_context, vec2s uv) { warp_uv(texture->wrap_mode, &uv); float lod = texture_get_sample_lod(texture, sample_context); return filter_texture(texture, uv, lod); } vec4s texture_sample_lod(const texture_t* texture, vec2s uv, float lod) { lod = glm_clamp(lod, 0.0f, texture->max_mip); warp_uv(texture->wrap_mode, &uv); return filter_texture(texture, uv, lod); } void texture_free(texture_t* texture) { if (texture != NULL && texture->data != NULL) { stbi_image_free(texture->data[0].data); for (uint8_t i = 1; i <= texture->max_mip; i++) { free(texture->data[i].data); } } }