#include #include #include #include #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" /* *********** * * * * Vec 3 * * * * *********** */ /* We can later use this to store 4 vecs for simd */ typedef struct { float x; float y; float z; } df_v3; 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; } /* ****************** * * * * List of hits * * * * ****************** */ typedef struct { df_v3 at; df_v3 normal; float ray_t; int front_face; /* 1 if we hit the front face */ } df_hit_record; static void set_face_normal(df_hit_record *record, df_v3 ray_dir, df_v3 outward_normal) { record->front_face = dot(ray_dir, outward_normal) < 0; record->normal = record->front_face ? outward_normal : (df_v3){-outward_normal.x, -outward_normal.y, -outward_normal.z}; } typedef struct { df_hit_record *hits; size_t count; size_t capacity; } df_hit_list; static void emit_hit(df_hit_list *list, df_hit_record record) { if (list->count == list->capacity) { size_t cap2 = (list->capacity > 0) ? 2 * list->capacity : 128; df_hit_record *tmp = realloc(list->hits, sizeof(df_hit_record) * cap2); if (!tmp) return; list->hits = tmp; list->capacity = cap2; } list->hits[list->count++] = record; } static df_hit_list merge_hit_lists(const df_hit_list **lists, unsigned int count) { size_t total_size = 0; for (unsigned int i = 0; i < count; ++i) total_size += lists[i]->count; df_hit_list out; out.count = total_size; out.hits = malloc(sizeof(df_hit_record) * total_size); if (!out.hits) return out; df_hit_record *dst = out.hits; for (unsigned int i = 0; i < count; ++i) { memcpy(dst, lists[i]->hits, sizeof(df_hit_record) * lists[i]->count); dst += lists[i]->count; } return out; } /* ********************* * * * * 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; } /* ********************************* * * * * 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(float ray_origin_x, float ray_origin_y, float ray_origin_z, float ray_dx, float ray_dy, float ray_dz, 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_dx * planes[i].normal_x + ray_dy * planes[i].normal_y + ray_dz * 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_dx; float py = ray_origin_y + t * ray_dy; float pz = ray_origin_z + t * ray_dz; 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; /* FIXME(Kevin): We would need to take plane rotation into account. * Alternatively, just pass w & h into the plane and let the user * (i.e. higher level code) worry about that */ float w = planes[i].img_p1_x - planes[i].img_p0_x; float h = planes[i].img_p1_y - planes[i].img_p0_y; 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; } 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; float aspect_ratio = (float)image_width / (float)image_height; float viewport_height = 2.f; float viewport_width = aspect_ratio * viewport_height; float focal_length = settings.focal_length; /* 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; uint8_t *pixels = malloc(image_width * image_height * 3); if (!pixels) return 0; float max_img_u = 0.f; float max_img_v = 0.f; 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); /* Target = Delta because origin is (0, 0, 0) */ float target_x = lower_left_x + u * viewport_width; float target_y = lower_left_y + v * viewport_height; float target_z = lower_left_z; /* Raycast against all spheres */ float sphere_hit_t = sphere_test(0, 0, 0, target_x, target_y, target_z, spheres, sphere_count); df_hit plane_hit = plane_test(0, 0, 0, target_x, target_y, target_z, 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); } } } *result = pixels; return 1; }