Wednesday, May 07, 2014

Shiny spheres

My friend told me that many years ago, she lost points in an MIT art class for putting the highlight in the wrong place on a sphere.  So when she saw my last post about radiance, she posed the question to me.

Turns out the answer is (-0.24,-0.25,-2.06).



Here's one where I moved the light source and just liked how it looked (before I added specular reflection):




// Projects a sphere onto the screen with ambient, diffuse and specular
// lighting, and prints the location of the specular highlight.
//
// Viewpoint is 0,0,0
// Screen is from -1,-1,-1 to 1,1,-1
// Sphere is at 0,0,-3 with radius 1
// Light source is at -1,-1,0 and projects
//
// Surface of the sphere emits ambient light in red, specular reflection in green and
// diffuse reflection in blue.
//
// Compile with gcc -o sphere sphere.c -lm -lfreeimage
// and make sure you have the libfreeimage-dev package installed.

#include <stdio.h>
#include <math.h>
#include <FreeImage.h>
#include <stdlib.h>

#define IMAGE_WIDTH 1000
#define IMAGE_HEIGHT 1000
#define SAMPLE_DENSITY 300

FIBITMAP *image;

void setup_image() {
  FreeImage_Initialise(FALSE);
  image = FreeImage_Allocate(IMAGE_WIDTH, IMAGE_HEIGHT, 24, 0, 0, 0);
  if (image == NULL) {
    printf("Failed to allocate image.\n");
    exit(1);
  }
}

void setpixel(double x, double y, double r, double g, double b) {
  RGBQUAD color;
  const double brightness_boost = 40.0;

  int red = fmin(r * 255 * brightness_boost, 255);
  int green = fmin(g * 255 * brightness_boost, 255);
  int blue = fmin(b * 255 * brightness_boost, 255);

  color.rgbRed = red;
  color.rgbGreen = green;
  color.rgbBlue = blue;
  FreeImage_SetPixelColor(image, x * IMAGE_WIDTH + (IMAGE_WIDTH / 2),
                                 y * IMAGE_HEIGHT + (IMAGE_HEIGHT / 2), &color);
  //printf("%lf,%lf\n", x, y);
}

void teardown() {
  FreeImage_Save(FIF_PNG, image, "out.png", 0);
  FreeImage_DeInitialise();
}

int main(int argc, char **argv) {
  const double pi = 3.14159;

  const double sphere_z = -3;
  const double light_x = -1;
  const double light_y = -1;
  const double light_z = -1;

  const double light_brightness = 1;
  const double specular_reflection_exponent = 10;
  const double reflected_brightness = 500;
  const double ambient_brightness = 0.2;

  double latitude;
  const double latitude_elements = SAMPLE_DENSITY / 2;
  const double latitude_step = pi / latitude_elements;

  setup_image();

  // For MIT's art department, keep track of where the specular highlight looks brightest
  double max_reflected_value = 0;
  double highlight_x = 0;
  double highlight_y = 0;
  double highlight_z = 0;

  // Walk over the sphere and compute light from the source and to the viewpoint at each spot.
  for (latitude = 0; latitude < pi; latitude += latitude_step) {
    double y = cos(latitude);

    double longitude;
    const double radius = sin(latitude);
    const double circumference = 2 * pi * radius;
    const double longitude_elements_at_equator = SAMPLE_DENSITY;
    const double longitude_step = (2 * pi) / longitude_elements_at_equator;

    for (longitude = 0; longitude < pi; longitude += longitude_step) {
      double x = radius * cos(longitude);
      double z = radius * sin(longitude);
      // Now we have x,y,z relative to the sphere center.  To get absolute coordinates,
      // we just have to translate in z, since the sphere is at 0,0,sphere_z.
      double absolute_z = z + sphere_z;

      double distance_to_viewpoint_squared = x*x + y*y + absolute_z*absolute_z;
      double distance_to_viewpoint = sqrt(distance_to_viewpoint_squared);

      // Dot product is x1x2 + y1y2 + z1z2 and yields cosine of the angle between
      // them if they're both normalized.
      // But x1=x2 and y1=y2, and |x,y,z|=1, so we only need to normalize by
      // distance_to_viewpoint.
      // This dot product is between the viewpoint and surface normal
      double view_dot_product = (x*x + y*y + (absolute_z * z)) / distance_to_viewpoint;

      // Now we'll calculate the dot product for the light source.
      // First, the vector from the sphere surface to the light
      double lx = light_x - x;
      double ly = light_y - y;
      double lz = light_z - absolute_z;
      double light_distance_squared = lx*lx + ly*ly + lz*lz;
      double light_distance = sqrt(light_distance_squared);
      // Now we can compute cos(angle to the light)
      double light_dot_product = (lx*x + ly*y + lz*z) / light_distance;
      // Cosine law for intensity and inverse square law
      double light_intensity = fmax(light_dot_product, 0) / light_distance_squared;
      // Reflection of light vector is light - 2(light dot normal)*normal
      // (also normalizing here)
      double reflected_x = (lx - 2*light_dot_product*x) / light_distance;
      double reflected_y = (ly - 2*light_dot_product*y) / light_distance;
      double reflected_z = (lz - 2*light_dot_product*z) / light_distance;
      // Now we compute cos(angle between reflection and viewpoint)
      double reflected_dot_product = (reflected_x*x + reflected_y*y + reflected_z*absolute_z)
        / distance_to_viewpoint;
      reflected_dot_product = fmax(reflected_dot_product, 0);
      // Raise to a power based on how shiny the surface is
      double reflected_intensity = pow(reflected_dot_product, specular_reflection_exponent);
     
      // Lambert's Law and inverse square law to viewpoint
      double view_intensity = fabs(view_dot_product) / distance_to_viewpoint_squared;

      double light_value = light_brightness * light_intensity * view_intensity;
      double ambient_value = ambient_brightness * view_intensity;
      double reflected_value = reflected_brightness * view_intensity * reflected_intensity;

      //printf("light:%.4lf ambient:%.4f\n", light_value, ambient_value);

      // If the sphere is lambertian, then the cosine of the angle between the surface
      // normal and the viewpoint gives the intensity.
      double projected_x = x / absolute_z;
      double projected_y = y / absolute_z;

      //printf("[lat=%.02lf lng=%.02lf (%.02lf,%.02lf,%.02lf) ", latitude, longitude, x, y, z);
      setpixel(projected_x, projected_y, ambient_value, reflected_value, light_value);

      if (reflected_value > max_reflected_value) {
        max_reflected_value = reflected_value;
        highlight_x = x;
        highlight_y = y;
        highlight_z = absolute_z;
      }
    }
  }

  printf("Brightest specular highlight was at (%.02f,%.02f,%.02f)\n",
      highlight_x, highlight_y, highlight_z);
  teardown();
  return 0;
}

No comments: