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;

}