#include #include #include #include #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include "pcg/pcg_basic.h" /* ************* * * * * Vectors * * * * ************* */ static df_v3 normalize(df_v3 v) { float l = sqrtf(v.x * v.x + v.y * v.y + v.z * v.z); return (df_v3){v.x / l, v.y / l, v.z / l}; } static float dot(df_v3 a, df_v3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } static float v2_len(df_v2 v) { return sqrtf(v.x * v.x + v.y * v.y); } /* ********************* * * * * Image functions * * * * ********************* */ #define MAX_IMAGES 1024 struct { struct { stbi_uc *pixels; int w; int h; } images[MAX_IMAGES]; unsigned int image_count; } _image_table; DF_API df_image_handle df_load_image(const char *path, int *w, int *h) { if (_image_table.image_count == MAX_IMAGES) return 0; int width, height, c; stbi_uc *pixels = stbi_load(path, &width, &height, &c, 4); if (!pixels) { return 0; } df_image_handle handle = (_image_table.image_count + 1); _image_table.image_count++; _image_table.images[handle].pixels = pixels; _image_table.images[handle].w = width; _image_table.images[handle].h = height; if (w) *w = width; if (h) *h = height; return handle; } /* *************************** * * * * Builtin camera models * * * * *************************** */ DF_API df_pinhole_camera_data df_create_pinhole_camera_data(int image_width, int image_height, float focal_length) { float aspect_ratio = (float)image_width / (float)image_height; float viewport_height = 2.f; float viewport_width = aspect_ratio * viewport_height; /* Simple perspective projection. * The lens is placed at (0, 0, 0) */ float lower_left_x = -viewport_width / 2.f; float lower_left_y = -viewport_height / 2.f; float lower_left_z = -focal_length; return (df_pinhole_camera_data){ .viewport_size = {.x = viewport_width, .y = viewport_height}, .lower_left = { .x = lower_left_x, .y = lower_left_y, .z = lower_left_z }, }; } DF_API df_ray df_pinhole_camera(float u, float v, unsigned int sample_idx, void *camera_data) { DF_UNUSED(sample_idx); df_pinhole_camera_data *data = camera_data; df_v3 origin = {0.f, 0.f, 0.f}; df_v3 target = { data->lower_left.x + u * data->viewport_size.x, data->lower_left.y + v * data->viewport_size.y, data->lower_left.z }; return (df_ray){ .origin = origin, .dir = target, }; } static df_v2 random_point_on_disk(unsigned int i) { for (unsigned int j = 0; j < i; ++j) pcg32_random(); while (1) { df_v2 p = {.x = (float)pcg32_boundedrand(1024) / 1023.f, .y = (float)pcg32_boundedrand(1024) / 1023.f}; if (v2_len(p) <= 1.f) return p; } } DF_API df_thin_lense_camera_data df_create_thin_lense_camera_data(int image_width, int image_height, float aperture, float focal_distance) { float aspect_ratio = (float)image_width / (float)image_height; float viewport_height = 2.f; float viewport_width = aspect_ratio * viewport_height; /* Simple perspective projection. * The lens is placed at (0, 0, 0) */ float lower_left_x = -viewport_width / 2.f; float lower_left_y = -viewport_height / 2.f; float lower_left_z = -focal_distance; float lens_radius = aperture / 2.f; return (df_thin_lense_camera_data){ .lens_radius = lens_radius, .lower_left = { .x = lower_left_x, .y = lower_left_y, .z = lower_left_z, }, .viewport_size = { .x = viewport_width, .y = viewport_height } }; } DF_API df_ray df_thin_lense_camera(float u, float v, unsigned int sample_idx, void *camera_data) { df_thin_lense_camera_data *data = camera_data; df_v2 lensp = random_point_on_disk(sample_idx); lensp.x *= data->lens_radius; lensp.y *= data->lens_radius; df_v3 offset = {u * lensp.x, v * lensp.y, .z = 0.f}; df_v3 origin = offset; df_v3 target = {data->lower_left.x + u * data->viewport_size.x + offset.x, data->lower_left.y + v * data->viewport_size.y + offset.y, data->lower_left.z + offset.z}; return (df_ray){.origin = origin, .dir = target}; } /* ********************************* * * * * Intersection test functions * * * * ********************************* */ typedef struct { float ray_t; float img_u; float img_v; df_image_handle image; } df_hit; /* -1 => does not hit, >= 0 => hit (sphere index) */ static float sphere_test(float ray_origin_x, float ray_origin_y, float ray_origin_z, float ray_dx, float ray_dy, float ray_dz, const df_sphere *spheres, unsigned int sphere_count) { float result = -1.f; for (unsigned int i = 0; i < sphere_count; ++i) { float delta_x = ray_origin_x - spheres[i].center_x; float delta_y = ray_origin_y - spheres[i].center_y; float delta_z = ray_origin_z - spheres[i].center_z; float a = ray_dx * ray_dx + ray_dy * ray_dy + ray_dz * ray_dz; float b = (delta_x * ray_dx + delta_y * ray_dy + delta_z * ray_dz); float c = (delta_x * delta_x + delta_y * delta_y + delta_z * delta_z) - (spheres[i].radius * spheres[i].radius); float discriminant = b * b - a * c; /* we can get the hit location t as: (-b - sqrtf(disciminant)) / (2.f * a) */ if (discriminant > 0) { float t = (-b - sqrtf(discriminant)) / (2.f * a); #if 0 df_hit_record hit; hit.at.x = ray_origin_x + t * ray_dx; hit.at.y = ray_origin_y + t * ray_dy; hit.at.z = ray_origin_z + t * ray_dz; hit.ray_t = t; df_v3 outward_normal = { hit.at.x - spheres[i].center_x, hit.at.y - spheres[i].center_y, hit.at.z - spheres[i].center_z }; set_face_normal(&hit, (df_v3){ray_dx, ray_dy, ray_dz}, outward_normal); #endif if (t > result) result = t; } } return result; } static df_hit plane_test(df_v3 ray_origin, df_v3 ray_d, const df_plane *planes, unsigned int plane_count) { df_hit result = { .ray_t = -1.f, }; for (unsigned int i = 0; i < plane_count; ++i) { float dot = (ray_d.x * planes[i].normal_x + ray_d.y * planes[i].normal_y + ray_d.z * planes[i].normal_z); if (dot > DF_EPSF32 || dot < -DF_EPSF32) { float delta_x = planes[i].base_x - ray_origin.x; float delta_y = planes[i].base_y - ray_origin.y; float delta_z = planes[i].base_z - ray_origin.z; float num = delta_x * planes[i].normal_x + delta_y * planes[i].normal_y + delta_z * planes[i].normal_z; float t = num / dot; if (t > result.ray_t) { result.ray_t = t; /* Project point on plane to image coordinate system */ float px = ray_origin.x + t * ray_d.x; float py = ray_origin.y + t * ray_d.y; float pz = ray_origin.z + t * ray_d.z; float img_p3_x = px - planes[i].img_p0_x; float img_p3_y = py - planes[i].img_p0_y; float img_p3_z = pz - planes[i].img_p0_z; float w = planes[i].img_w; float h = planes[i].img_h; result.img_u = img_p3_x * planes[i].img_ax0_x + img_p3_y * planes[i].img_ax0_y + img_p3_z * planes[i].img_ax0_z; result.img_v = img_p3_x * planes[i].img_ax1_x + img_p3_y * planes[i].img_ax1_y + img_p3_z * planes[i].img_ax1_z; result.img_u /= w; result.img_v /= h; result.image = planes[i].image; } } /* Line is parallel to the plane, either contained or not. Do we care? */ } return result; } /* Ray packets to support more than one ray (=sample) per pixel */ typedef struct { unsigned int samples_per_pixel; unsigned int ray_count; df_v3 *ray_origins; /* (0, 0, 0) is the center of the lens */ df_v3 *ray_deltas; /* not necessarily normalized! */ df_v2i *ray_pixels; /* pixel coordinates in the output image */ } df_ray_packet; DF_API int df_trace_rays(df_trace_rays_settings settings, const df_sphere *spheres, unsigned int sphere_count, const df_plane *planes, unsigned int plane_count, uint8_t **result) { int image_width = settings.image_width; int image_height = settings.image_height; unsigned int samples_per_pixel = (unsigned int)settings.samples_per_pixel; if (samples_per_pixel == 0) samples_per_pixel = 1; df_camera_fn camera = settings.camera; if (!camera) return 0; void *camera_data = settings.camera_data; uint8_t *pixels = malloc(image_width * image_height * 3); if (!pixels) return 0; memset(pixels, 0, image_width * image_height * 3); df_ray_packet packet; packet.samples_per_pixel = samples_per_pixel; packet.ray_count = image_width * image_height * samples_per_pixel; void *packet_mem = malloc(packet.ray_count * (2 * sizeof(df_v3) + sizeof(df_v2i))); if (!packet_mem) { free(pixels); return 0; } packet.ray_origins = (df_v3 *)packet_mem; packet.ray_deltas = packet.ray_origins + packet.ray_count; packet.ray_pixels = (df_v2i *)(packet.ray_deltas + packet.ray_count); /* FIXME(Kevin): Replace with dynamic (time() ?) initializer */ pcg32_srandom(1523789, 901842398023); /* Generate the rays */ unsigned int ray_idx = 0; for (int y = 0; y < image_height; ++y) { /* TODO(Kevin): SIMD */ uint8_t *row = pixels + y * image_width * 3; for (int x = 0; x < image_width; ++x) { float u = (float)x / (float)(image_width - 1); float v = (float)y / (float)(image_height - 1); for (unsigned int samplei = 0; samplei < samples_per_pixel; ++samplei) { packet.ray_pixels[ray_idx] = (df_v2i){x, y}; df_ray ray = camera(u, v, samplei, camera_data); packet.ray_origins[ray_idx] = ray.origin; packet.ray_deltas[ray_idx] = ray.dir; ++ray_idx; } #if 0 /* Target = Delta because origin is (0, 0, 0) */ df_v3 origin = {0.f, 0.f, 0.f}; df_v3 target = {lower_left_x + u * viewport_width, lower_left_y + v * viewport_height, lower_left_z}; /* Adjust for lens effect */ if (settings.lens_radius > 0.f) { df_v2 lensp = concentric_sample_disk((df_v2){u, v}); lensp.x *= settings.lens_radius; lensp.y *= settings.lens_radius; float ft = -focal_length / target.z; df_v3 focusp = {target.x * ft, target.y * ft, target.z * ft}; origin.x = lensp.x; origin.y = lensp.y; origin.z = 0.f; target = (df_v3){focusp.x - lensp.x, focusp.y - lensp.y, focusp.z}; } /* Raycast against all planes */ df_hit plane_hit = plane_test(origin, target, planes, plane_count); #if 0 float hits[] = {sphere_hit_t, plane_hit.ray_t}; float hit_t = df_max_f32(hits, DF_ARRAY_COUNT(hits)); if (hit_t >= 0) { df_v3 hit_p = {target_x * hit_t, target_y * hit_t, target_z * hit_t}; hit_p.z -= -1.f; df_v3 normal = normalize(hit_p); row[x * 3 + 0] = (uint8_t)((.5f * (normal.x + 1.f)) * 255); row[x * 3 + 1] = (uint8_t)((.5f * (normal.y + 1.f)) * 255); row[x * 3 + 2] = (uint8_t)((.5f * (normal.z + 1.f)) * 255); } #endif if (plane_hit.ray_t >= 0) { float img_u = plane_hit.img_u; float img_v = plane_hit.img_v; // Temporary, handle arbitrary scale (esp. resulting from different orientation) float view_aspect = viewport_width / viewport_height; if (img_u >= 0 && img_v >= 0 && img_u <= 1.f && img_v <= 1.f) { int pixelx = (int)floorf(img_u * (_image_table.images[plane_hit.image].w - 1)); int pixely = (int)floorf(img_v * (_image_table.images[plane_hit.image].h - 1)); stbi_uc *pixel = _image_table.images[plane_hit.image].pixels + 4 * (pixely * _image_table.images[plane_hit.image].w + pixelx); row[x * 3 + 0] = pixel[0]; row[x * 3 + 1] = pixel[1]; row[x * 3 + 2] = pixel[2]; } } else { /* Gradient background color */ float len = sqrtf(target.x * target.x + target.y * target.y + target.z * target.z); float t = .5f * (target.y / len + 1.f); float r = (1.f - t) + t * 0.5f; float g = (1.f - t) + t * 0.7f; float b = (1.f - t) + t * 1.0f; row[x * 3 + 0] = (uint8_t)(r * 255); row[x * 3 + 1] = (uint8_t)(g * 255); row[x * 3 + 2] = (uint8_t)(b * 255); } #endif } } /* Trace the rays */ for (unsigned int i = 0; i < packet.ray_count; ++i) { df_v2i pixelp = packet.ray_pixels[i]; uint8_t *row = pixels + pixelp.y * image_width * 3; df_hit plane_hit = plane_test(packet.ray_origins[i], packet.ray_deltas[i], planes, plane_count); if (plane_hit.ray_t >= 0) { float img_u = plane_hit.img_u; float img_v = plane_hit.img_v; if (img_u >= 0 && img_v >= 0 && img_u <= 1.f && img_v <= 1.f) { int pixelx = (int)floorf(img_u * (_image_table.images[plane_hit.image].w - 1)); int pixely = (int)floorf(img_v * (_image_table.images[plane_hit.image].h - 1)); stbi_uc *pixel = _image_table.images[plane_hit.image].pixels + 4 * (pixely * _image_table.images[plane_hit.image].w + pixelx); row[pixelp.x * 3 + 0] += pixel[0] / samples_per_pixel; row[pixelp.x * 3 + 1] += pixel[1] / samples_per_pixel; row[pixelp.x * 3 + 2] += pixel[2] / samples_per_pixel; } } } free(packet_mem); *result = pixels; return 1; }