Files
SimpleRayTracing/source/Rendering/Texture.c
Misaki e1693764f7 Add mipmap support and refactor texture handling
Note: Currently version still have lots of fireflies after applying normal map. And those fireflies are mostly coming from the nee sky. Need to double check the sky cdf and ray intersection.

Changed the `hit_result_t` structure to rename a parameter in `RayIntersection.h`.
Removed the `normal` field from the `path_output` structure in `Common.h`.
Added new fields `screen_size`, `camera_position`, and `camera_direction` in `Material.h`.
Changed the `mipmap_t` structure in `Texture.h` to include a `max_mip` field and modify the `data` field.
Changed the `texture_load` function in `Texture.c` to include a `mipmap` parameter and improve texture data handling.
Changed the `path_trace` function in `PathTracing.c` to update the `shading_context_t` structure and ray creation.
Changed the `evaluate_bsdf_directional` function in `LightEvaluation.c` to modify angular radius calculation.
Changed the `sky_create_hdr_sky` and `evaluate_bsdf_hdr_sky` functions in `SkyLight.c` to enhance texture sampling.
Changed the `get_surface_data` function in `SimpleLit.c` to incorporate camera distance and view direction in calculations.
Changed the `texture_get_pixel` function in `Texture.c` to improve pixel data retrieval.
Changed the `warp_uv` function to use a `vec2s` structure for UV coordinates.
Changed the `texture_sample` function to include additional parameters for improved sampling accuracy.
Changed the `scene_setup` function in `main.c` to adjust sun light intensity and HDRI texture loading.
2025-05-06 17:46:42 +09:00

486 lines
16 KiB
C

#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_entity_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_entity();
}
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_entity();
}
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 = REPEAT;
texture.filter_mode = LINEAR;
if (textures->count >= textures->size)
{
texture_collection_resize(textures, textures->size * 2);
}
texture_entity_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 REPEAT:
uv->x = fmodf(fabsf(uv->x), 1.0f);
uv->y = fmodf(fabsf(uv->y), 1.0f);
break;
case 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);
}
float texture_get_sample_lod(vec3s view_direction, vec3s normal, float distance)
{
// TODO: Implement a more accurate LOD calculation based on the distance to the surface and the view direction.
const float factor = 1.0f;
float n_dot_v = glms_vec3_dot(normal, view_direction);
return fmaxf(log2f(distance * factor) - fabsf(n_dot_v), 0.0f);
}
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 = glm_clamp((float)x0, 0.0f, (float)mipmap->width - 1);
x1 = glm_clamp((float)x1, 0.0f, (float)mipmap->width - 1);
y0 = glm_clamp((float)y0, 0.0f, (float)mipmap->height - 1);
y1 = glm_clamp((float)y1, 0.0f, (float)mipmap->height - 1);
// 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 NEAREST:
return nearest_filter(texture, uv, (uint8_t)lod);
case 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, vec2s uv, vec3s view_direction, vec3s normal, float distance)
{
warp_uv(texture->wrap_mode, &uv);
return filter_texture(texture, uv, texture_get_sample_lod(view_direction, normal, distance));
}
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);
}
}
}