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;
}