tinyray/tinyray.c
2020-11-20 02:02:48 +13:00

450 lines
No EOL
14 KiB
C

// ENGGEN131 (2020) - Lab 9 (5th - 9th October, 2020)
// EXERCISE SIX - Da Vinci Code
//
// tinyray.c - A raytracer in exactly 400 lines of C
// Author: Matthew Jakeman
//
// Entirely original code, inspired by the following resources:
// - https://github.com/ssloy/tinyraytracer
// - https://www.gabrielgambetta.com/computer-graphics-from-scratch/basic-ray-tracing.html
// - https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <math.h>
// A single byte-type representing one channel of a pixel
typedef unsigned char byte;
/* Minimal Floating Point Vector Maths - Author: Matthew Jakeman */
typedef struct {
float x;
float y;
float z;
} Vec3;
Vec3 vec3_new(float x, float y, float z) {
Vec3 vec;
vec.x = x;
vec.y = y;
vec.z = z;
return vec;
}
Vec3 vec3_add(Vec3 a, Vec3 b) {
return vec3_new(a.x + b.x, a.y + b.y, a.z + b.z);
}
Vec3 vec3_subtract(Vec3 a, Vec3 b) {
return vec3_new(a.x - b.x, a.y - b.y, a.z - b.z);
}
Vec3 vec3_subtract_scalar(Vec3 a, float b) {
return vec3_new(a.x - b, a.y - b, a.z - b);
}
Vec3 vec3_divide_scalar(Vec3 a, float b) {
return vec3_new(a.x/b, a.y/b, a.z/b);
}
Vec3 vec3_multiply(Vec3 a, Vec3 b) {
return vec3_new(a.x*b.x, a.y*b.y, a.z*b.z);
}
Vec3 vec3_multiply_scalar(Vec3 a, float b) {
return vec3_new(a.x*b, a.y*b, a.z*b);
}
float vec3_dot(Vec3 a, Vec3 b) {
return a.x*b.x + a.y*b.y + a.z*b.z;
}
float vec3_len(Vec3 v) {
return (float)sqrtf((v.x * v.x) + (v.y * v.y) + (v.z * v.z));
}
Vec3 vec3_normalise(Vec3 v) {
return vec3_divide_scalar(v, vec3_len(v)); // Calculate unit vector
}
Vec3 vec3_clamp(Vec3 v, float min_value, float max_value) {
return vec3_new(
fmin(fmax(v.x, min_value), max_value),
fmin(fmax(v.y, min_value), max_value),
fmin(fmax(v.z, min_value), max_value)
);
}
// END - vectors
/* Struct Definitions */
typedef Vec3 RgbColour;
typedef struct {
RgbColour diffuse;
float specular;
} Material;
typedef struct {
Vec3 centre;
float radius;
Material material;
} Sphere;
typedef struct {
Vec3 origin;
Vec3 direction;
} Ray;
typedef enum {
PointLight,
DirectionalLight,
AmbientLight
} LightType;
typedef struct {
LightType type;
Vec3 position; // point only
Vec3 direction; // directional only
float intensity;
} Light;
/* Globals */
const static Vec3 Zero = {0};
const static Vec3 Invalid = {-1, -1, -1};
const static int FOV = 1; //3.1415/2;
const static int MAX_DIST = 1000;
const static unsigned int WIDTH = 600;
const static unsigned int HEIGHT = 600;
/* Constructors */
Sphere sphere_new(Vec3 centre, float radius, Material material) {
Sphere sphere = {0};
sphere.centre = centre;
sphere.radius = radius;
sphere.material = material;
return sphere;
}
Light light_point_new(float intensity, Vec3 position) {
Light light = {0};
light.type = PointLight;
light.position = position;
light.intensity = intensity;
return light;
}
Light light_ambient_new(float intensity) {
Light light = {0};
light.type = AmbientLight;
light.intensity = intensity;
return light;
}
Light light_directional_new(float intensity, Vec3 direction) {
Light light = {0};
light.type = DirectionalLight;
light.direction = direction;
light.intensity = intensity;
return light;
}
Material material_new(Vec3 diffuse, float specular) {
Material material = {0};
material.diffuse = diffuse;
material.specular = specular;
return material;
}
Ray ray_new(Vec3 origin, Vec3 direction) {
Ray ray;
ray.origin = origin;
ray.direction = direction;
return ray;
}
// Lighting Compute Algorithm
// Adapted from https://www.gabrielgambetta.com/
//
// ARGS:
// - point = point of intersection
// - normal = normal vector at point
// RETURNS:
// - intensity of light
float lighting_compute(Vec3 point, Vec3 normal,
Vec3 view, float specular,
Light *lights, int num_lights) {
// Intensity of light for the given pixel
float intensity = 0.0f;
// Iterate over lights
for (int i = 0; i < num_lights; i++)
{
Light *light = &lights[i];
if (light->type == AmbientLight)
{
// Simply add ambient light to total
intensity += light->intensity;
}
else
{
Vec3 light_ray;
if (light->type == PointLight)
// Point Light: Direction of ray from light to point
light_ray = vec3_subtract(light->position, point);
else
// Directional Light: Direction
light_ray = light->direction;
// Diffuse
float reflect = vec3_dot(normal, light_ray);
intensity += (light->intensity * reflect)/(vec3_len(normal) * vec3_len(light_ray));
// Specular
if (specular != -1)
{
Vec3 r = vec3_subtract(vec3_multiply_scalar(normal, 2 * vec3_dot(normal, light_ray)), light_ray);
float reflect_view_proj = vec3_dot(r, view);
if (reflect_view_proj > 0)
{
float cosine = reflect_view_proj/(vec3_len(r) * vec3_len(view));
intensity += light->intensity * powf(cosine, specular);
}
}
}
}
return intensity;
}
// Sphere-Ray Intersection
// Returns 1 if intersection found, otherwise 0
//
// ARGS:
// - sphere = sphere to intersect
// - ray = description of ray properties (e.g. direction)
// RETURNS:
// - boolean of whether ray intersected a sphere
// - [out] dist0, dist 1 = perpendicular distances to points of intersection
int do_sphere_raycast(Sphere sphere, Ray ray, float *dist0, float *dist1) {
// Please see sphere_ray_intersection.bmp
*dist0 = 0;
*dist1 = 0;
// Find L and tca
Vec3 L = vec3_subtract(sphere.centre, ray.origin);
float tca = vec3_dot(L, ray.direction);
// Discard if intersection is behind origin
if (tca < 0)
return 0;
// Find d
float d = sqrtf(vec3_dot(L, L) - tca * tca);
if (d > sphere.radius)
return 0;
// Calculate thc using pythagoras
float thc = sqrtf(sphere.radius * sphere.radius - d * d);
// Calculate t0 and t1 (perpendicular distance to
// the 0th and 1st intersection)
float t0 = tca - thc;
float t1 = tca + thc;
// Ensure at least one of t0 and t1 is greater than zero
if (t0 < 0 && t1 < 0)
return 0;
*dist0 = t0;
*dist1 = t1;
return 1; // Intersection found
}
// Raytrace Scene at Point
//
// ARGS:
// - origin = location of camera
// - dir = direction of projectile
// - min_t, max_t = min and max clipping planes
// - spheres, num_spheres = array of spheres to test
// - lights, num_lights = array of lights in the scene
// RETURNS:
// - rgb colour of pixel being raytraced
RgbColour raytrace(Vec3 origin, Vec3 dir, float min_t, float max_t,
Sphere *spheres, int num_spheres,
Light *lights, int num_lights) {
// Closest sphere to screen (for depth-testing)
Sphere *closest = 0;
// We use t_comp to store the t-depth of the closest
// sphere and compare it with other spheres to perform
// primitive depth testing (where 't' is perpendicular
// distance to the point of intersection)
float t_comp = (float)MAX_DIST;
// Ray to test
Ray ray = ray_new(origin, dir);
// Cycle through all spheres and depth-test
for (int i = 0; i < num_spheres; i++)
{
float dist0, dist1;
if (do_sphere_raycast(spheres[i], ray, &dist0, &dist1))
{
// Check dist0
if ((min_t < dist0 && dist0 < max_t) &&
dist0 < t_comp)
{
t_comp = dist0;
closest = &spheres[i];
}
// Now check dist1
if ((min_t < dist1 && dist1 < max_t) &&
dist1 < t_comp)
{
t_comp = dist1;
closest = &spheres[i];
}
}
}
if (!closest)
return Invalid;
Vec3 point = vec3_add(origin, vec3_multiply_scalar(dir, t_comp));
Vec3 normal = vec3_normalise(vec3_subtract(point, closest->centre));
Material material = closest->material;
return vec3_clamp(
vec3_multiply_scalar(
material.diffuse,
lighting_compute(
point, normal,
vec3_multiply_scalar(dir, -1),
material.specular,
lights, num_lights
)
),
0.0f, 255.0f // Clamp between 0 and 255
);
}
// Watermark: Says "MATT J"
#define MARK_COLS 33
#define MARK_ROWS 7
int mark[MARK_ROWS][MARK_COLS] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0},
{0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
// Draws a watermark on the screen using the above array
// NOTE: Make sure size and stride do not cause the function to exceed
// array bounds. This will cause a crash!
void DrawWatermark(byte *data) {
int start_x = 20;
int start_y = 540;
int size = 3, stride = 1;
for (int i = 0; i < MARK_COLS; i++) {
for (int j = 0; j < MARK_ROWS; j++) {
int y_corner = start_y + j*(size*2 + stride);
int x_corner = start_x + i*(size*2 + stride);
// Draw Square
for (int x = x_corner; x < (x_corner + size); x++) {
for (int y = y_corner; y < (y_corner + size); y++) {
if (mark[j][i] == 0) {
data[(y*WIDTH + x) * 3 + 0] = (byte)(i/(float)MARK_COLS * 255) % 180;
data[(y*WIDTH + x) * 3 + 1] = (byte)(j/(float)MARK_ROWS * 255) % 180;
data[(y*WIDTH + x) * 3 + 2] = (byte)240;
}
else {
data[(y*WIDTH + x) * 3 + 0] = (byte)255;
data[(y*WIDTH + x) * 3 + 1] = (byte)255;
data[(y*WIDTH + x) * 3 + 2] = (byte)255;
}
}
}
}
}
}
int main(void)
{
byte *data = malloc(sizeof(byte) * 3 * WIDTH * HEIGHT);
// Materials
Material blue = material_new(vec3_new(69, 161, 255), 500);
Material white = material_new(vec3_new(240, 240, 240), 180);
Material red = material_new(vec3_new(255, 0, 57), 10);
Material ground = material_new(vec3_new(0, 57, 89), 1000);
// Scene
#define NUM_SPHERES 4
Sphere spheres[NUM_SPHERES];
spheres[0] = sphere_new(vec3_new(-0.75f, -0.2f, 6.5f), 1.5f, red);
spheres[1] = sphere_new(vec3_new(0, -1, 5), 1.0f, blue);
spheres[2] = sphere_new(vec3_new(2, -0.5, 8), 3.0f, white);
spheres[3] = sphere_new(vec3_new(0, -4001, 0), 4000, ground);
// Lights
#define NUM_LIGHTS 3
Light lights[NUM_LIGHTS];
lights[0] = light_ambient_new(0.2f);
lights[1] = light_point_new(0.6f, vec3_new(-8, 1, 0));
lights[2] = light_directional_new(0.2f, vec3_new(1, 4, -8));
// For non-square images (future-proofing?)
float aspect_ratio = (float)WIDTH/(float)HEIGHT;
float screen_dim = tanf(FOV / (float)2);
Vec3 origin = Zero;
// Render
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
// Background
data[(y*WIDTH + x) * 3 + 0] = (byte)(y/(float)WIDTH * 255);
data[(y*WIDTH + x) * 3 + 1] = (byte)(x/(float)HEIGHT * 255);
data[(y*WIDTH + x) * 3 + 2] = (byte)160;
// Get Pixel in World Coords
float x_world_coord = (2*(x + 0.5f)/(float)HEIGHT - 1) * screen_dim * aspect_ratio;
float y_world_coord = -(2*(y + 0.5f)/(float)WIDTH - 1) * screen_dim;
Vec3 dir = vec3_normalise(vec3_new(x_world_coord, y_world_coord, 1));
// Raytrace Pixel
RgbColour colour = raytrace(origin, dir, 1.0f, (float)MAX_DIST,
spheres, NUM_SPHERES,
lights, NUM_LIGHTS);
// Draw Geometry
if (colour.x != -1) {
data[(y*WIDTH + x) * 3 + 0] = (byte)colour.x;
data[(y*WIDTH + x) * 3 + 1] = (byte)colour.y;
data[(y*WIDTH + x) * 3 + 2] = (byte)colour.z;
}
}
}
// Output
DrawWatermark(data);
// Write to file
printf("tinyray: writing to file!");
if (!stbi_write_bmp("output.bmp", WIDTH, HEIGHT, 3, data))
printf("tinyray: failed to write image!");
return 0;
}