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