From e1693764f75e5c66a6519358a50cb0ae2369c03b Mon Sep 17 00:00:00 2001 From: Misaki Date: Tue, 6 May 2025 17:46:35 +0900 Subject: [PATCH] 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. --- header/Algorithm/RayIntersection.h | 2 +- header/Common.h | 1 - header/Material/Material.h | 5 + header/Rendering/Texture.h | 23 +- source/Algorithm/PathTracing.c | 9 +- source/Algorithm/RayIntersection.c | 14 +- source/Geometry/Mesh.c | 2 +- source/Lighting/LightEvaluation.c | 2 +- source/Lighting/SkyLight.c | 20 +- source/Material/SimpleLit.c | 25 +- source/Rendering/Texture.c | 362 ++++++++++++++++++++++++----- source/main.c | 5 +- 12 files changed, 366 insertions(+), 104 deletions(-) diff --git a/header/Algorithm/RayIntersection.h b/header/Algorithm/RayIntersection.h index 7511af2..867c4f7 100644 --- a/header/Algorithm/RayIntersection.h +++ b/header/Algorithm/RayIntersection.h @@ -28,7 +28,7 @@ typedef struct } hit_result_t; ray_t ray_create(vec3s origin, vec3s direction); -vec3s offset_ray_origin(vec3s point, vec3s normal, vec3s wo); +vec3s offset_ray_origin(vec3s point, vec3s normal, vec3s w); hit_result_t ray_intersect_triangle(const ray_t* ray, const triangle_t* triangle); bool ray_intersect_aabb(const ray_t* ray, aabb_t aabb, float* enter_out, float* exit_out); diff --git a/header/Common.h b/header/Common.h index cca7389..988907c 100644 --- a/header/Common.h +++ b/header/Common.h @@ -29,7 +29,6 @@ typedef enum typedef struct { vec3s wi; - vec3s normal; vec3s direct_lighting; vec3s bsdf; float pdf; diff --git a/header/Material/Material.h b/header/Material/Material.h index 9bad89e..3766ace 100644 --- a/header/Material/Material.h +++ b/header/Material/Material.h @@ -13,6 +13,11 @@ typedef struct { + vec4s screen_size; // w, h, 1/w, 1/h + + vec3s camera_position; + vec3s camera_direction; + vec3s position; vec3s normal; vec3s tangent; diff --git a/header/Rendering/Texture.h b/header/Rendering/Texture.h index 01e5d2a..be210d7 100644 --- a/header/Rendering/Texture.h +++ b/header/Rendering/Texture.h @@ -32,11 +32,22 @@ typedef struct { uint32_t width; uint32_t height; + char* data; +} mipmap_t; + +typedef struct +{ + vec2s texel_size; + uint32_t width; + uint32_t height; + wrap_mode_t wrap_mode; filter_mode_t filter_mode; - uint8_t channel_count; stride_t stride; - char* data; + + uint8_t channel_count; + uint8_t max_mip; + mipmap_t* data; } texture_t; typedef struct @@ -61,9 +72,11 @@ bool texture_collection_init(uint16_t size, texture_collection_t* textures); void texture_collection_resize(texture_collection_t* textures, uint16_t size); void texture_collection_free(texture_collection_t* textures); -texture_entity_t texture_load(const char* filename, bool srgb, stride_t stride, texture_collection_t* textures); -vec4s texture_get_pixel(const texture_t* texture, uint32_t x, uint32_t y); -vec4s texture_sample(const texture_t* texture, float u, float v); +texture_entity_t texture_load(const char* filename, bool srgb, bool mipmap, stride_t stride, texture_collection_t* textures); +vec4s texture_get_pixel(const texture_t* texture, vec2s uv, uint8_t lod); +float texture_get_sample_lod(vec3s view_direction, vec3s normal, float distance); +vec4s texture_sample(const texture_t* texture, vec2s uv, vec3s view_direction, vec3s normal, float distance); +vec4s texture_sample_lod(const texture_t* texture, vec2s uv, float lod); void texture_free(texture_t* texture); inline texture_entity_t invalid_texture_entity() diff --git a/source/Algorithm/PathTracing.c b/source/Algorithm/PathTracing.c index 5bf2444..a2925ea 100644 --- a/source/Algorithm/PathTracing.c +++ b/source/Algorithm/PathTracing.c @@ -32,6 +32,9 @@ vec4s path_trace(const scene_t* scene, ray_t ray, uint32_t sample_index, uint16_ const material_t* hit_material = &scene->materials.buffer[scene->triangles.buffer[closest_hit.triangle_id].material_id]; shading_context_t shading_context = { + .camera_position = scene->camera.position, + .camera_direction = glms_vec3_normalize(glms_vec3_sub(closest_hit.point, scene->camera.position)), + .position = closest_hit.point, .normal = closest_hit.normal, .tangent = closest_hit.tangent, @@ -52,7 +55,6 @@ vec4s path_trace(const scene_t* scene, ray_t ray, uint32_t sample_index, uint16_ accumulated_color = glms_vec4_add(accumulated_color, glms_vec4(material_output.direct_lighting, 0.0f)); pdf_bsdf = material_output.pdf; - float cos_theta = fmaxf(0.0f, glms_vec3_dot(material_output.wi, closest_hit.normal)); throughput = glms_vec3_mul(throughput, material_output.bsdf); // We do Russian roulette to decide whether to continue tracing or terminate the path @@ -76,7 +78,7 @@ vec4s path_trace(const scene_t* scene, ray_t ray, uint32_t sample_index, uint16_ // active_ray = ray_create(BIAS_RAY_ORIGION(closest_hit.point, glms_vec3_negate(closest_hit.normal)), active_ray.direction); // continue; default: - active_ray = ray_create(offset_ray_origin(closest_hit.point, material_output.normal, shading_context.wo), material_output.wi); + active_ray = ray_create(offset_ray_origin(closest_hit.point, closest_hit.normal, shading_context.wo), material_output.wi); depth++; break; } @@ -99,6 +101,9 @@ void render_aov(const scene_t* scene, ray_t ray, uint32_t sample_index, uint16_t const material_t* hit_material = &scene->materials.buffer[scene->triangles.buffer[closest_hit.triangle_id].material_id]; shading_context_t shading_context = { + .camera_position = scene->camera.position, + .camera_direction = glms_vec3_normalize(glms_vec3_sub(closest_hit.point, scene->camera.position)), + .position = closest_hit.point, .normal = closest_hit.normal, .tangent = closest_hit.tangent, diff --git a/source/Algorithm/RayIntersection.c b/source/Algorithm/RayIntersection.c index d4db14c..a7a27e2 100644 --- a/source/Algorithm/RayIntersection.c +++ b/source/Algorithm/RayIntersection.c @@ -16,7 +16,7 @@ ray_t ray_create(vec3s origin, vec3s direction) ((direction.z < 0.0f) ? 4 : 0) }; - ray.esp = glms_vec3_max(glms_vec3_abs(ray.origin)) * gamma(10); + ray.esp = glms_vec3_max(glms_vec3_abs(ray.origin)) * gamma(15); return ray; } @@ -40,14 +40,16 @@ static inline float next_float_down(float value) return nextafterf(value, -INFINITY); } -vec3s offset_ray_origin(vec3s point, vec3s normal, vec3s wo) +vec3s offset_ray_origin(vec3s point, vec3s normal, vec3s w) { - vec3s abs_normal = glms_vec3_abs(normal); - float c = glms_vec3_max(glms_vec3_abs(point)) * gamma(10); - float d = glms_vec3_dot(abs_normal, (vec3s){c, c, c}); + float c = glms_vec3_max(glms_vec3_abs(point)) * gamma(15); + vec3s error = (vec3s){c, c, c}; + // float g = gamma(10); + // vec3s error = {fabsf(point.x) * g, fabsf(point.y) * g, fabsf(point.z) * g}; + float d = glms_vec3_dot(glms_vec3_abs(normal), error); vec3s offset = glms_vec3_scale(normal, d); - if (glms_vec3_dot(glms_vec3_negate(wo), normal) < 0.0f) + if (glms_vec3_dot(glms_vec3_negate(w), normal) < 0.0f) { offset = glms_vec3_negate(offset); } diff --git a/source/Geometry/Mesh.c b/source/Geometry/Mesh.c index 0bef724..ee29575 100644 --- a/source/Geometry/Mesh.c +++ b/source/Geometry/Mesh.c @@ -19,7 +19,7 @@ static texture_entity_t load_material_texture(const struct aiMaterial* material, string_join(directory, image_path, path.data, 1024); } - return texture_load(path.data, true, UINT_8, &scene->textures); + return texture_load(path.data, true, true, UINT_8, &scene->textures); } return invalid_texture_entity(); } diff --git a/source/Lighting/LightEvaluation.c b/source/Lighting/LightEvaluation.c index ab4d959..7df4f53 100644 --- a/source/Lighting/LightEvaluation.c +++ b/source/Lighting/LightEvaluation.c @@ -13,7 +13,7 @@ path_output evaluate_bsdf_directional(directional_light_t light, const light_sha return output; } - float angular_radius = glm_rad(light.angular_diameter / 2.0f); + float angular_radius = glm_rad(light.angular_diameter * 0.5f); uint16_t d1 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_U); uint16_t d2 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_V); diff --git a/source/Lighting/SkyLight.c b/source/Lighting/SkyLight.c index a901039..5410ff2 100644 --- a/source/Lighting/SkyLight.c +++ b/source/Lighting/SkyLight.c @@ -148,7 +148,8 @@ sky_light_t sky_create_hdr_sky(const texture_collection_t* textures, texture_ent for (uint32_t j = 0; j < data.width; ++j) { - vec4s pixel = texture_get_pixel(hdri, j, i); + vec2s uv = {(float)j / (float)data.width, (float)i / (float)data.height}; + vec4s pixel = texture_get_pixel(hdri, uv, 0); float lum = luminance(glms_vec3(pixel)); // * sin_theta; p_xy_original_pdf[i * data.width + j] = lum; @@ -297,7 +298,7 @@ path_output evaluate_bsdf_hdr_sky(const void* data, const light_shading_context_ if (context->bvh_tree == NULL) { vec2s uv = direction_to_equirectangular(context->wo); - vec4s sky_light = texture_sample(get_texture(context->textures, sky_data->texture), uv.x, uv.y); + vec4s sky_light = texture_sample_lod(get_texture(context->textures, sky_data->texture), uv, 0); output.direct_lighting = glms_vec3_scale(glms_vec3_mul(glms_vec3(sky_light), throughput), sky_data->intensity); output.pdf = 1.0f / (4.0f * PI); return output; @@ -305,12 +306,12 @@ path_output evaluate_bsdf_hdr_sky(const void* data, const light_shading_context_ uint32_t i,j; // Should we also use sobol numbers for the sky sampling? - //uint16_t d1 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_U); - //uint16_t d2 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_V); - //float r1 = sobol_sample(sample_index, d1); - //float r2 = sobol_sample(sample_index, d2); - float r1 = random_float(); - float r2 = random_float(); + uint16_t d1 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_U); + uint16_t d2 = sobol_get_dimension(context->bounce_depth, PRNG_LIGHT_V); + float r1 = sobol_sample(sample_index, d1); + float r2 = sobol_sample(sample_index, d2); + //float r1 = random_float(); + //float r2 = random_float(); uv_to_index((vec2s){r1, r2}, sky_data->width, sky_data->height, &j, &i); vec3s cdf = sky_data->cdf_cache[i * sky_data->width + j]; @@ -323,7 +324,6 @@ path_output evaluate_bsdf_hdr_sky(const void* data, const light_shading_context_ } ray_t shadow_ray = ray_create(offset_ray_origin(context->position, context->normal, context->wo), wi); - hit_result_t shadow_hit = {0}; ray_intersect_bvh_any(&shadow_ray, context->bvh_tree->nodes, context->bvh_tree->primitive_indices, context->bvh_tree->triangles, 0, &shadow_hit); if (shadow_hit.hit) @@ -334,7 +334,7 @@ path_output evaluate_bsdf_hdr_sky(const void* data, const light_shading_context_ vec2s uv = direction_to_equirectangular(wi); const texture_t* hdri = get_texture(context->textures, sky_data->texture); - vec4s pixel = texture_sample(hdri, uv.x, uv.y); + vec4s pixel = texture_sample_lod(hdri, uv, 0); vec3s sky_light = glms_vec3_scale(glms_vec3(pixel), sky_data->intensity); float pdf = hdr_pdf(sky_data->cdf_cache, wi, sky_data->height, sky_data->width); diff --git a/source/Material/SimpleLit.c b/source/Material/SimpleLit.c index 33531c7..44967c9 100644 --- a/source/Material/SimpleLit.c +++ b/source/Material/SimpleLit.c @@ -12,11 +12,14 @@ static void get_surface_data(const shading_context_t* context, const void* prope { const simple_lit_properties_t* prop = (simple_lit_properties_t*)properties; + float camera_distance = glms_vec3_distance(context->camera_position, context->position); + vec3s view = (context->camera_direction); + data_out->albedo = prop->albedo; const texture_t* albedo_texture = get_texture(context->textures, prop->albedo_texture); if (albedo_texture != NULL && albedo_texture->data != NULL) { - data_out->albedo = glms_vec3_mul(data_out->albedo, glms_vec3(texture_sample(albedo_texture, context->uv.x, context->uv.y))); + data_out->albedo = glms_vec3_mul(data_out->albedo, glms_vec3(texture_sample(albedo_texture, context->uv, view, context->normal, camera_distance))); } // NOTE: Currently normal map will produce lots of fireflies, need to be fixed. @@ -25,7 +28,7 @@ static void get_surface_data(const shading_context_t* context, const void* prope const texture_t* normal_texture = get_texture(context->textures, prop->normal_texture); if (normal_texture != NULL && normal_texture->data != NULL) { - vec3s normal_sample = glms_vec3(texture_sample(normal_texture, context->uv.x, context->uv.y)); + vec3s normal_sample = glms_vec3(texture_sample(normal_texture, context->uv, view, context->normal, camera_distance)); normal_sample = normal_unpack(normal_sample); data_out->normal = normal_ts_to_ws(normal_sample, context->normal, context->tangent); } @@ -34,14 +37,14 @@ static void get_surface_data(const shading_context_t* context, const void* prope const texture_t* roughness_texture = get_texture(context->textures, prop->roughness_texture); if (roughness_texture != NULL && roughness_texture->data != NULL) { - data_out->roughness = data_out->roughness * texture_sample(roughness_texture, context->uv.x, context->uv.y).x; + data_out->roughness = data_out->roughness * texture_sample(roughness_texture, context->uv, view, context->normal, camera_distance).x; } data_out->metallic = prop->metallic; const texture_t* metallic_texture = get_texture(context->textures, prop->metallic_texture); if (metallic_texture != NULL && metallic_texture->data != NULL) { - data_out->metallic = data_out->metallic * texture_sample(metallic_texture, context->uv.x, context->uv.y).x; + data_out->metallic = data_out->metallic * texture_sample(metallic_texture, context->uv, view, context->normal, camera_distance).x; } } @@ -88,15 +91,16 @@ static vec3s sample_bsdf_simple_lit(const shading_context_t* context, const surf // We can use a inversion sampling where cos(theta) = powf(random_float(), 1.0f / (specular_exponent + 1.0f)) and phi = 2 * PI * random_float() float specular_exponent = roughness_to_blinn_phong_specular_exponent(surface_data->roughness); - float theta = acosf(powf(sobol_sample(sample_index, d1), 1.0f / (specular_exponent + 1.0f))); + // float theta = acosf(powf(sobol_sample(sample_index, d1), 1.0f / (specular_exponent + 1.0f))); + float cos_theta = powf(sobol_sample(sample_index, d1), 1.0f / (specular_exponent + 1.0f)); + float sin_theta = sin_from_cos(cos_theta); float phi = 2.0f * PI * sobol_sample(sample_index, d2); - // float theta = acosf(powf(random_float(), 1.0f / (specular_exponent + 1.0f))); - // float phi = 2.0f * (float)M_PI * random_float(); + vec3s h_ts = (vec3s) { - sinf(theta) * cosf(phi), - sinf(theta) * sinf(phi), - cosf(theta) + sin_theta * cosf(phi), + sin_theta * sinf(phi), + cos_theta }; vec3s tangent_u; // World-space tangent (U) @@ -259,7 +263,6 @@ path_output simple_lit_render_loop(const shading_context_t* properties, const sh vec3s bsdf = evaluate_bsdf_simple_lit(context, &surface_data, output.wi); float cos_theta = fmaxf(0.0f, glms_vec3_dot(output.wi, surface_data.normal)); - output.normal = surface_data.normal; output.bsdf = glms_vec3_scale(bsdf, cos_theta / output.pdf); output.state = PS_NORMAL; return output; diff --git a/source/Rendering/Texture.c b/source/Rendering/Texture.c index 0c6f674..5e3047a 100644 --- a/source/Rendering/Texture.c +++ b/source/Rendering/Texture.c @@ -64,7 +64,195 @@ void texture_collection_free(texture_collection_t* textures) textures->buffer = NULL; } -texture_entity_t texture_load(const char* filename, bool srgb, stride_t stride, texture_collection_t* textures) + +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 @@ -77,33 +265,47 @@ texture_entity_t texture_load(const char* filename, bool srgb, stride_t stride, // } int width, height, channels; - char* data = NULL; + char* raw_data = NULL; switch (stride) { case UINT_8: - data = stbi_load(filename, &width, &height, &channels, 0); + raw_data = (char*)stbi_load(filename, &width, &height, &channels, 0); break; case UINT_16: - data = (char*)stbi_load_16(filename, &width, &height, &channels, 0); + raw_data = (char*)stbi_load_16(filename, &width, &height, &channels, 0); break; case FLOAT_32: - data = (char*)stbi_loadf(filename, &width, &height, &channels, 0); + raw_data = (char*)stbi_loadf(filename, &width, &height, &channels, 0); break; } - if (data == NULL) + 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 = data; + texture.data = temp_texture_data; texture.wrap_mode = REPEAT; texture.filter_mode = LINEAR; @@ -121,131 +323,163 @@ texture_entity_t texture_load(const char* filename, bool srgb, stride_t stride, return entity; } -static inline void warp_uv(wrap_mode_t mode, float* u, float* v) +static inline void warp_uv(wrap_mode_t mode, vec2s* uv) { switch (mode) { case REPEAT: - *u = fmodf(fabsf(*u), 1.0f); - *v = fmodf(fabsf(*v), 1.0f); + uv->x = fmodf(fabsf(uv->x), 1.0f); + uv->y = fmodf(fabsf(uv->y), 1.0f); break; case CLAMP: - *u = fminf(fmaxf(*u, 0.0f), 1.0f); - *v = fminf(fmaxf(*v, 0.0f), 1.0f); + *uv = glms_vec2_clamp(*uv, 0.0f, 1.0f); break; } } -vec4s texture_get_pixel(const texture_t* texture, uint32_t x, uint32_t y) +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) { - if (x >= texture->width || y >= texture->height) - { - return (vec4s){0.0f, 0.0f, 0.0f, 1.0f}; - } + size_t pixel_start_offset = (size_t)(y * width + x) * channel_count * stride; - uint32_t pixel_index = y * texture->width + x; - size_t pixel_offset = (size_t)pixel_index * texture->channel_count * texture->stride; + vec4s out = {0.0f, 0.0f, 0.0f, 1.0f}; - uint8_t* base8 = (uint8_t*)texture->data + pixel_offset; - uint16_t* base16 = (uint16_t*)(texture->data) + ((y * texture->width + x) * texture->channel_count); - float* base32 = (float*)(texture->data) + ((y * texture->width + x) * texture->channel_count); - - vec4s out = {0, 0, 0, 1}; // default alpha = 1 - - for (int c = 0; c < texture->channel_count && c < 4; ++c) + for (int c = 0; c < channel_count && c < 4; c++) { float value = 0.0f; - switch (texture->stride) + 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 = base8[c] / 255.0f; + value = (float)(((uint8_t*)data)[channel_offset]) / 255.0f; break; case UINT_16: - value = base16[c] / 65535.0f; + value = (float)(*((uint16_t*)(data + channel_offset))) / 65535.0f; break; case FLOAT_32: - value = base32[c]; + value = *((float*)(data + channel_offset)); break; default: value = (c == 3) ? 1.0f : 0.0f; break; } - out.raw[c] = value; } return out; } -static vec4s nearest_filter(const texture_t* texture, float u, float v) +vec4s texture_get_pixel(const texture_t* texture, vec2s uv, uint8_t lod) { - uint32_t x = (uint32_t)floorf(u * (texture->width - 1)); - uint32_t y = (uint32_t)floorf(v * (texture->height - 1)); + 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}; + } - x = x < texture->width ? x : texture->width - 1; - y = y < texture->height ? y : texture->height - 1; + uint32_t x = (uint32_t)floorf(uv.x * (mipmap->width - 1)); + uint32_t y = (uint32_t)floorf(uv.y * (mipmap->height - 1)); - return texture_get_pixel(texture, x, y); + 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); } -static vec4s linear_filter(const texture_t* texture, float u, float v) +float texture_get_sample_lod(vec3s view_direction, vec3s normal, float distance) { - float x = u * (texture->width - 1); - float y = v * (texture->height - 1); + // 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 x1 = x0 + 1; 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; - // Clamp to edges - x0 = x0 < texture->width ? x0 : texture->width - 1; - x1 = x1 < texture->width ? x1 : texture->width - 1; - y0 = y0 < texture->height ? y0 : texture->height - 1; - y1 = y1 < texture->height ? y1 : texture->height - 1; + 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); - // Sample 4 texels - vec4s c00 = texture_get_pixel(texture, x0, y0); - vec4s c10 = texture_get_pixel(texture, x1, y0); - vec4s c01 = texture_get_pixel(texture, x0, y1); - vec4s c11 = texture_get_pixel(texture, x1, y1); + // 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); - // Interpolate along x - vec4s c0 = glms_vec4_lerp(c00, c10, sx); - vec4s c1 = glms_vec4_lerp(c01, c11, sx); - - // Interpolate along y - vec4s result = glms_vec4_lerp(c0, c1, sy); + 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, float u, float v) +static inline vec4s filter_texture(const texture_t* texture, vec2s uv, float lod) { switch (texture->filter_mode) { case NEAREST: - return nearest_filter(texture, u, v); + return nearest_filter(texture, uv, (uint8_t)lod); case LINEAR: - return linear_filter(texture, u, v); + 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, float u, float v) +vec4s texture_sample(const texture_t* texture, vec2s uv, vec3s view_direction, vec3s normal, float distance) { - warp_uv(texture->wrap_mode, &u, &v); - return filter_texture(texture, u, v); + 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); + stbi_image_free(texture->data[0].data); + for (uint8_t i = 1; i <= texture->max_mip; i++) + { + free(texture->data[i].data); + } } } diff --git a/source/main.c b/source/main.c index 717205f..3386182 100644 --- a/source/main.c +++ b/source/main.c @@ -32,7 +32,7 @@ static bool scene_setup(scene_t* scene) directional_light_t* sun_light = &scene->lights.directional_lights[sun.id]; sun_light->direction = glms_vec3_normalize((vec3s){0.6f, 1.0f, 0.25f}); sun_light->color = (vec3s){1.0f, 0.93f, 0.87f}; - sun_light->intensity = 1.0f; + sun_light->intensity = 2.0f; sun_light->angular_diameter = 0.53f; //scene->lights.sky_light = sky_create_constant_sky(&(constant_sky_data_t) @@ -40,7 +40,8 @@ static bool scene_setup(scene_t* scene) // .color = (vec3s){0.73f, 0.82f, 1.0f}, // .intensity = 1.0f, //}); - texture_entity_t hdri = texture_load(HDRI_PATH, false, FLOAT_32, &scene->textures); + texture_entity_t hdri = texture_load(HDRI_PATH, false, false, FLOAT_32, &scene->textures); + scene->textures.buffer[hdri.id].texture.wrap_mode = CLAMP; scene->lights.sky_light = sky_create_hdr_sky(&scene->textures, hdri, 1.0f); return scene->lights.sky_light.data != NULL;