//package lightingdemo; // Written by: Chris Weigle // Date: November 15, 2003 import java.awt.*; import java.applet.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; /* SphereLight - illuminate a hemisphere on a plane. Light vector tracks the * mouse. View vector is fixed at z+. All calculations use the same light * vector (infinite light source) and the same view vector (infinite viewer). * It's faster, but still looks okay. */ public class SphereLightPanel extends JComponent implements MouseMotionListener { protected static final int WIDTH = 300; protected static final int HEIGHT = 300; protected static final float RADIUS = 125; protected int buffer[] = null; // holds the image while we compute it protected float normals[][] = null; // holds the normals for out geometry protected float light[] = {0, 0, 1}; // this is the light vector protected float viewer[] = {0, 0, 1}; // this is the view vector protected float color[] = {1, 1, 1}; // the is the material color protected float ka = .2f, kd = .5f, ks = .7f, ksp = 25.f; // Phong parameters protected BufferedImage image = null; // this copies the image to the screen public SphereLightPanel() { setPreferredSize(new Dimension(WIDTH, HEIGHT)); buffer = new int[WIDTH*HEIGHT]; image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); initialize(); illumination(); addMouseMotionListener(this); buffer = new int[WIDTH*HEIGHT]; image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); } public void mouseDragged(MouseEvent event) {} /* * mouseMoved - when the mouse moves, move the light. * Assume the mouse is RADIUS + a little above the plane. * Compute a new light vector. */ public void mouseMoved(MouseEvent event) { light[0] = (float)event.getX() - (float)WIDTH*.5f; light[1] = (float)event.getY() - (float)HEIGHT*.5f; light[2] = (float)RADIUS*1.25f; // convert to a unit vector (length of 1) float r = 1.f / (float)Math.sqrt(Dot(light, light)); light[0] *= r; light[1] *= r; light[2] *= r; illumination(); repaint(); } /* initialize() : Fill the normals[] array with normal vectors * Geometry: assume an XY plane (normal in the z+ direction) with a * sphere centered. * Since we're doing infinite viewre and light, we don't need to keep up * with the position, only the surface normal. So that's all we'll store. */ protected void initialize() { normals = new float[WIDTH*HEIGHT][3]; for (int i = 0; i < HEIGHT; i++) { double y = ((float)i - (float)HEIGHT*.5) / RADIUS; // temp y-position for (int j = 0; j < WIDTH; j++) { double x = ((float)j - (float)WIDTH*.5) / RADIUS, // temp x-pos r2 = 1. - Math.sqrt(x*x+y*y); // z-position squared (sphere) int pixel = i*WIDTH+j; if (r2 < 0) // if r2 is < 0, then we're beyond the radius of the sphere { normals[pixel][0] = 0; // just a XY plane, normal points z+ normals[pixel][1] = 0; normals[pixel][2] = 1; } else { double l = 1./Math.sqrt(r2+x*x+y*y); // record the unit normal vector normals[pixel][0] = (float)(x*l); normals[pixel][1] = (float)(y*l); normals[pixel][2] = (float)(Math.sqrt(r2)*l); } } } } /* Dot : Compute the dot product of two vectors. This is used pretty often. * V1 dot V2 = |V1| * |V2| * cos(angle between V1 and V2) * Where |V| is the length of vector V * So if V1 and V2 are unit vectors (|V1| = |V2| = 1) then * V1 dot V2 gives the cosine of the angle between them. It also happens * to be the length of V1 projected onto V2 (the fraction of V1 that points * in the same direction as V2). This leads to the folloing nice properties: * If V1 and V2 point in opposite directions, dot < 0. * If V1 and V2 are perpendicualr, dot = 0. * If V1 and V2 are the same unit vector, dot = 1. */ protected float Dot(float v1[], float v2[]) { return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2]; } /* illumination: compute the color at each pixel according to the Phong * illumination model * * I = k_ambient * + k_diffuse * (Normal dot Light) * + k_specular * (Reflection dot View) ^ k_specular_exponent * * I - total intensity of light arriving at the viewer * k_ambient - intensity of light due to global scattering * (not from any particular source, think daylight) * k_diffuse - fraction of light scattered uniformly as by a rough surface * k_specular - fraction of light scattered in the reflection direction as * by a mirror * k_specular_exponent - fall-off of specular reflection as view vector * differs from reflection vector * * Reflection Vector: * * N * L | R The angle between N and L is the same as tha angle * \ | / between N and R (N is the surface normal, L is the * \ | / light vector, R is the reflection vector. * \ | / * ____\|/____ * * Here's how we figure out R from only N and L. * * 2N(NdotL) |\ -L NdotL is the fraction of L pointing in the N direction. * | \ N(NdotL) is then the projection of L onto N. 2N(NdotL) * | \ is then twice that. If we then subtract L, we end up * | \ with a vector pointing in the R direction. Really. * L \ | / R * \ | / * \ | / * ___\|/___ * * A couple notes: * If NdotL is less than zero, than assume the surface faces away from the * light and drop the diffuse and specular terms. * If RdotV is less than zero, than drop the specular term. */ protected void illumination() { for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { int pixel = i*WIDTH+j; float ndotl = Dot(normals[pixel], light), c[] = {0, 0, 0}, illum = ka; if (ndotl > 0) { illum += ndotl * kd; // This is the full computation // float r[] = {0, 0, 0}; // // r[0] = 2 * ndotl * normals[pixel][0] - light[0]; // r[1] = 2 * ndotl * normals[pixel][1] - light[1]; // r[2] = 2 * ndotl * normals[pixel][2] - light[2]; // // float rdotv = Dot(r, viewer); // if (rdotv > 0) illum += ks * Math.pow(rdotv, ksp); // But, we have an infinite viewer at (0, 0, 1), so we don't // have to do the X or Y components of the computation float r = 2 * ndotl * normals[pixel][2] - light[2], rdotv = r * viewer[2]; if (rdotv > 0) illum += ks * Math.pow(rdotv, ksp); } illum = illum > 1 ? 1 : illum; // clamp to 1 // rescale range to 0-255, 'cause that's what we need for display illum *= 255; c[0] = color[0] * illum; c[1] = color[1] * illum; c[2] = color[2] * illum; buffer[pixel] = (255 << 24) + ((int)c[0] << 16) + ((int)c[1] << 8) + ((int)c[2] << 0); } } image.setRGB(0, 0, WIDTH, HEIGHT, buffer, 0, WIDTH); } public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D)g; g2.drawImage(image, 0, 0, this); } }