// COPYRIGHT 2001 KEN PERLIN
import java.applet.*; import java.awt.*; import java.awt.image.*; public class Bucky extends PixApplet { // CALLED AT THE START OF EACH FRAME public void initFrame(int frame) { // IF FIRST TIME THROUGH if (S == null) { // GENERATE THE SPHERES defineSpheres(); // COMPUTE THE SPHERE DISK SILHOUETTES R = W/31; S = new int[2][colors.length][2*R+1][2*R+1]; d = new int[nd][2*R+1][2*R+1]; dd = new int[nd][2*R+1][2]; int c, rgb[] = new int[3]; double RGB[] = new double[3]; for (int x = -R ; x <= R ; x++) for (int y = -R ; y <= R ; y++) { // DIFF DISK FOR IN FOCUS (FRONT) AND OUT OF FOCUS (BACK) SPHERES for (int u = 0 ; u < nd ; u++) { double fuzz = (1.5*u + 2) * R; d[u][R+y][R+x] = f2i(disk(x*x + y*y, R, fuzz)); } // SHADE AS FROSTED (k=0) OR POLISHED (k=1) if (d[0][R+y][R+x] > 0) for (int i = 0 ; i < colors.length ; i++) { int k = (i!=0&&i!=4 ? 1 : 0); // SPHERES IN FRONT ARE SHADED BRIGHTER for (int j = 0 ; j < 3 ; j++) RGB[j] = colors[i][j] * 1.1; S[0][i][R+y][R+x] = shade((double)x/R,(double)y/R,RGB,k); // SPHERES IN BACK ARE SHADED DARKER for (int j = 0 ; j < 3 ; j++) RGB[j] = colors[i][j] * 0.7; S[1][i][R+y][R+x] = shade((double)x/R,(double)y/R,RGB,k); } } // SPEEDUP: COMPUTE EACH DISK'S FIRST/LAST PIXEL IN EACH SCAN-LINE for (int u = 0 ; u < nd ; u++) for (int y = -R ; y <= R ; y++) { int x = -R; for ( ; x <= R ; x++) if (d[u][R+y][R+x] != 0) break; dd[u][R+y][0] = x; for ( ; x <= R ; x++) if (d[u][R+y][R+x] == 0) break; dd[u][R+y][1] = x; } // DRAW THE BACKGROUND GRAD NEAR THE TOP OF THE PICTURE fill(0, H/5, W, H - H/5, BG); for (int y = 0 ; y < H/5 ; y++) { double t = (double)(y - H/5) / (0 - H/5); fill(0, y, W, 1, (int)(BG + 3*t*t * (SH - BG))); } damage = true; } } // SET PIXEL VALUES FOR ONE FRAME public void setPix(int frame) { // FORCE A REFRESH EVERY 30 FRAMES if (frame % 30 == 0) damage = true; if (!damage) return; // INITIALIZE THE FRAME initFrame(frame); // COMPUTE THE BACK-TO-FRONT DISPLAY ORDER FOR THE SPHERES orderSpheres(); // ERASE PREVIOUS FRAME'S SPHERES AND SHADOWS fill(W/5, H/5, 3*W/5,3*H/5, BG); // ERASE PREVIOUS SPHERES fill( 0,3*H/5, 2*W/3,2*H/5, BG); // ERASE PREVIOUS SHADOWS // DRAW SHADOW IN MULTIPLE LAYERS: FROM PENUMBRA TO FULLSHADOW int ns = 6; for (int m = ns-1 ; m >= 0 ; m--) { double t = Math.pow((double)m / ns, 0.7); int sh = (int)(SH + t*(BG-SH)); for (int n = 0 ; n < nxyz ; n++) drawShadow(d[0], dd[0], pX(xyz[n]), pY(xyz[n])/2, sh, m); } // THEN DRAW ALL THE SPHERES, IN BACK-TO-FRONT ORDER for (int n = 0 ; n < nxyz ; n++) { int u = Math.min(nd-1, (int)((.5 - xyz[n][2])/.12)); int v = xyz[n][2] > -.18 ? 0 : 1; drawSphere(S[v][(int)xyz[n][3]],d[u],dd[u],pX(xyz[n]),pY(xyz[n])); } } // FILL A RECTANGLE WITH A CONSTANT GRAY LEVEL private void fill(int x, int y, int w, int h, int gray) { rgb[0] = rgb[1] = rgb[2] = gray; int packed = pack(rgb); for (int Y = y ; Y < y + h ; Y++) { int i = xy2i(x, Y); for (int X = x ; X < x + w ; X++) pix[i++] = packed; } } // COMPUTE AT WHICH PIXEL A SPHERE PROJECTS ONTO THE IMAGE private int pX(double xyz[]) { // X COORDINATE OF PROJECTION return (int)(W/2 + W/2 * xyz[0]); } private int pY(double xyz[]) { // Y COORDINATE OF PROJECTION return (int)(H/2 - W/2 * xyz[1]); } // DRAW ONE SPHERE SHADOW private void drawShadow(int d[][],int dd[][],int ix,int iy,int sh,int p) { rgb[0] = rgb[1] = rgb[2] = sh; int shadow = pack(rgb); double dY = (double)(R + 2*p) / R; for (int Y = 1-R-2*p ; Y <= R+2*p ; Y += 2) { int y = (int)(Y / dY); int dy[] = d[R+y]; int x0 = dd[R+y][0] - 5*p, x1 = dd[R+y][1] + p; int i = xy2i(ix + 2 - W/6, iy + Y/2 + H/2 + 30) + x0; for(int x = x0; x < x1; x++) pix[i++] = shadow; } } // DRAW ONE SPHERE private void drawSphere(int s[][], int d[][], int dd[][], int ix, int iy) { int D, rgb1[] = new int[3], rgb2[] = new int[3]; for(int y = -R; y <= R; y++) { int sy[] = s[R+y]; int dy[] = d[R+y]; int x0 = dd[R+y][0], x1 = dd[R+y][1]; int i = xy2i(ix, iy+y) + x0; for(int x = x0; x < x1; x++) pix[i++] = dy[R+x]==255 ? sy[R+x] : blend(i,sy[R+x],dy[R+x]); } } // METHOD TO BLEND SMOOTHLY OVER BACKGROUND; USED NEAR SILHOUETTE OF SPHERE. private int rgb1[] = new int[3], rgb2[] = new int[3]; private int blend(int i, int p2, int d) { unpack(rgb1, pix[i]); unpack(rgb2, p2); for (int j = 0 ; j < 3 ; j++) rgb2[j] = rgb1[j] + ((rgb2[j]-rgb1[j]) * d >> 8); return pack(rgb2); } // COMPUTE THE ANTIALIASED DISK IMAGE OF A SPHERE SILHOUETTE private double disk(double rr, double R, double fuzz) { double RR = R*R; return rr > RR-1 ? 0 : rr > RR-fuzz ? 1-(rr-RR)/fuzz : 1; } // SHADE A SPHERE private int rgb[] = new int[3]; private int shade(double x, double y, double RGB[], int type) { double z = Math.sqrt(1 - x*x - y*y); double d, ss1, ss2, s1, s2, ss; double R = RGB[0], G = RGB[1], B = RGB[2]; s1 = -2*x+6*z; s2 = 4*y-x+6*z; ss1 = (0.7*x-1.5*y+2.5*z) / 3.24; ss2 = (1.5*x-0.7*y+2.5*z) / 3.14; switch (type) { case 0: // IF FROSTED SURFACE ss = ( Math.pow((1+ss1)/2, 37) + Math.pow((1+ss2)/2, 37) ) * .6; // AMBIENT ILLUMINATION s1 = Math.pow((1+s1/6.6)/2, 20) * .40; // FIRST HILITE s2 = Math.pow((1+s2/8.6)/2, 25) * .35; // SECOND HILITE d = (x-y) / 1.414; d = (.2 + .6 * d*d) * (R+G+B) + (s1 + s2) * .67; // DIFFUSE rgb[0] = f2i(R*d + G*s1 + B*s2 + ss); rgb[1] = f2i(G*d + B*s1 + R*s2 + ss); rgb[2] = f2i(B*d + R*s1 + G*s2 + ss); break; case 1: // IF POLISHED SURFACE ss = ( Math.pow((1+ss1)/2, 67) + Math.pow((1+ss2)/2, 67) ) * 4; // AMBIENT ILLUMINATION s1 = Math.pow((1+s1/7.6)/2, 27) * 3.50; // FIRST HILITE s2 = Math.pow((1+s2/7.6)/2, 11) * 0.70; // SECOND HILITE rgb[0] = f2i(R*(s1 + s2) + ss); rgb[1] = f2i(G*(s1 + s2) + ss); rgb[2] = f2i(B*(s1 + s2) + ss); break; } return pack(rgb); } // CONVERT A FLOATING POINT VALUE TO A 0..255 INTEGER private int f2i(double t) { return (int)(255 * t) & 255; } // INITIALIZE THE PLACEMENT AND COLOR SCHEME FOR SPHERES private void defineSpheres() { setView(); I = J = K = 10; s = new int[I][J][K]; xyz = new double[I * J * K][4]; for(int i = 0; i < I; i++) for(int j = 0; j < J; j++) for(int k = 0; k < K; k++) { computeXYZ(i, j, k); double d = X * X + Y * Y + Z * Z; // COMPUTER RADIUS SQUARED double d1 = 0.25; // R-SQUARED FOR OUTER-MOST SPHERES double d2 = 0.17; // R-SQUARED FOR INNER-MOST SPHERES if (d < d1 && d > d2) { // IF R-SQUARED IS BETWEEN d1 AND d2 s[i][j][k] = // PLACE A SPHERE; VARY COLOR W. RADIUS 1 + (int)(colors.length * (d - d2) / (d1 - d2)); } } } // COMPUTE PROPER BACK TO FRONT ORDER FOR TRAVERSING SPHERES private void orderSpheres() { // OUTER LOOP WILL BE WHICHEVER OF i,j, OR k CHANGES FASTEST IN Z double di = pz(1, 0, 0) - pz(0, 0, 0); double dj = pz(0, 1, 0) - pz(0, 0, 0); double dk = pz(0, 0, 1) - pz(0, 0, 0); double ai = Math.abs(di); double aj = Math.abs(dj); double ak = Math.abs(dk); byte b = ai<=aj || ai<=ak ? aj<=ai || aj<=ak ? (byte)2 : 1 : 0; nxyz = 0; for(int n = 0; n < I * J * K; n++) { int i = ( b != 0 ? b != 1 ? n : n / J : n / J / K ) % I; int j = ( b != 1 ? b != 2 ? n : n / K : n / K / I ) % J; int k = ( b != 2 ? b != 0 ? n : n / I : n / I / J ) % K; // IN EACH DIMENSION, SCAN ORDER IS FROM FURTHEST TO NEAREST SPHERES if (di < 0) i = I-1 - i; if (dj < 0) j = J-1 - j; if (dk < 0) k = K-1 - k; // IF A SPHERE IS AT GRID PT, ROTATE TO VIEW COORDS AND ADD TO LIST if (s[i][j][k] != 0) { computeXYZ(i,j,k); xyz[nxyz][0] = X; xyz[nxyz][1] = Y; xyz[nxyz][2] = Z; xyz[nxyz][3] = s[i][j][k] - 1; nxyz++; } } } // ROTATE THE VIEW OF A POINT, DEPENDING ON USER DEFINED ROTATION ANGLES private void computeXYZ(double i, double j, double k) { double x1 = (i - (double)(I / 2)) / (double)(I - 1); double y1 = (j - (double)(J / 2)) / (double)(I - 1); double z1 = (k - (double)(K / 2)) / (double)(I - 1); double x2 = CP * x1 + SP * y1; // ROTATE LATITUDE BY PHI double y2 = SP * x1 - CP * y1; double z2 = z1; X = CT * x2 - ST * z2; // ROTATE LONGITUDE BY THETA Y = y2; Z = ST * x2 + CT * z2; } // RETURN THE Z COORDINATE OF THE CENTER OF THE SPHERE AT (i,j,k) ON GRID private double pz(int i, int j, int k) { computeXYZ(i, j, k); return Z; } // UPDATE THE VIEW ROTATION VARIABLES private void setView() { CT = Math.cos(theta); ST = Math.sin(theta); CP = Math.cos(phi); SP = Math.sin(phi); } // RESPOND TO USER MOUSE DOWN BY REMEMBERING CURSOR POSITION public boolean mouseDown(Event event, int x, int y) { mx = x; my = y; return true; } // RESPOND TO USER MOUSE DRAG BY ROTATING VIEW public boolean mouseDrag(Event event, int x, int y) { theta += 0.03 * (double)(mx - x); // HORIZONTAL MOTION CHANGES THETA phi -= 0.03 * (double)(my - y); // VERTICAL MOTION CHANGES PHI setView(); mx = x; my = y; damage = true; return true; } // INTERNAL VARIABLES private int S[][][][]; // PIXEL IMAGE FOR EACH TYPE OF SPHERE private int d[][][]; // DISK IMAGE FOR VARIOUSLY DEFOCUSED SPHERES private int dd[][][]; // X START/STOP FOR EACH SCAN-LINE OF DISK IMAGE private int nd = 5; // HOW MANY DIFFERENT LEVELS OF FOCUS private int R; // SPHERE RADIUS private int BG = 110, SH = 94; // BACKGROUND AND SHADOW GRAY LEVELS private int I, J, K; // THE GRID DIMENSIONS private int nxyz; // NUMBER OF SPHERES TO DISPLAY private double xyz[][]; // VIEW XYZs, IN DISPLAY ORDER private int s[][][]; // SPHERE COLOR AT EACH GRID POINT private double X, Y, Z; // VIEW XYZ FOR ONE SPHERE private double F; // CAMERA FOCAL LENGTH private double theta = 1, phi = .5; // USER-DEFINED VIEW ROTATION ANGLES private double CT, ST, CP, SP; // INTERNAL VIEW ROTATION VARIABLES private int mx, my; // CURRENT MOUSE POSITION // ALL THE PRETTY COLORS private double colors[][] = { {0.60, 0.50, 0.00}, // gold {0.60, 0.40, 0.10}, // amber {0.68, 0.20, 0.20}, // ruby {0.10, 0.10, 0.55}, // sapphire {0.50, 0.35, 0.40}, // satin }; }