/* koyomi7.c - Don Yang (uguu.org) 09/29/09 */ #include #include #include #include #include #define OBJECT_COUNT 7 double /* Global clock, updated exactly once per frame */ CurrentTime, /* Stat counter */ StartTime, /* Tile image for cloud texture */ EnvCloud[1025][1025], /* second dimension: keyframe0, keyframe1, current frame third dimension: x, y, z, rotate x, rotate y, rotate z, time, face */ TaraiObject[OBJECT_COUNT][3][8], /* Gradient data */ top[3] = {107, 192, 229}, middle[3] = {240, 250, 250}, bottom[3] = {0, 128, 64}, t1 = 0.40, t2 = 0.39, /* Previously local variables */ dx, dy, r2, ly, ty, p; int /* Stat counter */ CurrentFrame, /* Precompiled object */ TaraiObjectList, /* Texture image */ TextureObject, /* Contour curve (X, Y, normal X, normal Y). Bottom center of the tarai is at (0, 0). */ curve[9][4] = { {69, -27, 0, -1}, {75, -24, 3, -1}, {90, 21, 3, -1}, {93, 21, 0, -1}, {96, 24, 1, 0}, {93, 27, 0, 1}, {87, 27, -3, 1}, {72, -18, -3, 1}, {69, -21, 0, 1} }, /* Previously local variables */ i, j, x, y, cx, cy, nx, ny; /* Texture pixels */ unsigned char TextureImage[512][512][3]; /* Convert value from one range to another */ double ConvertRange(double input0, double input1, double output0, double output1, double value) { return (output1 - output0) * (value - input0) / (input1 - input0) + output0; } /* Convert value from one range to another, keeping only the remainder for out of bound values. */ int ModulusRange(double value) { i = (int)(512 * (value + 1)); return (i < 0) ? 1024 - ((-i) & 1023) : i & 1023; } /* Generate a random number for a particular range */ double RandomNumber(double output0, double output1) { return ConvertRange(0, RAND_MAX, output0, output1, rand()); } /* Update pixel in cloud image */ void SetCloudPixel(int sx, int sy, double value) { if( !j || (sx && sx - 1024 && sy && sy - 1024) ) EnvCloud[sy][sx] = value; } /* Subdivide & interpolate cloud image recursively */ void SubDivideCloud(int step, double roughness) { for(; step > 1;) { step /= 2; for(y = 0; y < 1024; y = ny) { cy = y + step; ny = cy + step; for(x = 0; x < 1024; x = nx) { cx = x + step; nx = cx + step; /* Add center point */ SetCloudPixel(cx, cy, (EnvCloud[y][x] + EnvCloud[y][nx] + EnvCloud[ny][x] + EnvCloud[ny][nx]) / 4); /* Add edge points */ SetCloudPixel(cx, y, (EnvCloud[y][x] + EnvCloud[y][nx] + EnvCloud[cy][cx]) / 3); SetCloudPixel(x, cy, (EnvCloud[y][x] + EnvCloud[cy][cx] + EnvCloud[ny][x]) / 3); SetCloudPixel(nx, cy, (EnvCloud[y][nx] + EnvCloud[cy][cx] + EnvCloud[ny][nx]) / 3); SetCloudPixel(cx, ny, (EnvCloud[cy][cx] + EnvCloud[ny][x] + EnvCloud[ny][nx]) / 3); /* Display newly added points */ SetCloudPixel(cx, y, EnvCloud[y][cx] + RandomNumber(-1, 1) * roughness); SetCloudPixel(x, cy, EnvCloud[cy][x] + RandomNumber(-1, 1) * roughness); SetCloudPixel(cx, cy, EnvCloud[cy][cx] + RandomNumber(-1, 1) * roughness); SetCloudPixel(nx, cy, EnvCloud[cy][nx] + RandomNumber(-1, 1) * roughness); SetCloudPixel(cx, ny, EnvCloud[ny][cx] + RandomNumber(-1, 1) * roughness); } } roughness /= 2; } } /* Set pixel from a gradient of 2 colors */ void SetGradient(double *color0, double *color1, double value) { for(i = 0; i < 3; i++) { TextureImage[y][x][i] = (unsigned char)ConvertRange(0, 1, color0[i], color1[i], value); } } /* Set pixel color based on gradient position in [-1, 1] */ void SetTexturePixel(void) { if( ty > t1 ) { SetGradient(middle, top, ConvertRange(t1, 1, 0, 1, ty)); } else if( ty > t2 ) { for(i = 0; i < 3; i++) TextureImage[y][x][i] = (unsigned char)middle[i]; } else { SetGradient(bottom, middle, ConvertRange(-1, t2, 0, 1, ty)); } } /* Generate new keyframe for object */ void GenerateNewKeyframe(void) { /* Replace keyframes */ for(j = 0; j < 8; j++) TaraiObject[i][0][j] = TaraiObject[i][1][j] = TaraiObject[i][2][j]; /* Generate new keyframe, making sure that the object will bounce off of a different face. Note that we prefer to not bounce off the Z faces, because motion in that direction tend to look too subtle due to how our camera is placed. */ for(TaraiObject[i][1][6] += RandomNumber(2, 7); TaraiObject[i][2][7] == TaraiObject[i][0][7]; TaraiObject[i][2][7] = floor(RandomNumber(0, 4))); for(j = 0; j < 6; j++) TaraiObject[i][1][j] = RandomNumber(-360, 360); j = (int)TaraiObject[i][2][7]; TaraiObject[i][1][j / 2] = ((j & 1) ? -360 : 360); } /* Update animation time */ void UpdateClock(void) { struct timeval t; gettimeofday(&t, NULL); CurrentTime = t.tv_sec + t.tv_usec / 1e6; CurrentFrame++; } /* Animate and render a single frame */ void Render(void) { UpdateClock(); glDrawBuffer(GL_BACK); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* Set projection */ x = glutGet(GLUT_WINDOW_WIDTH); y = glutGet(GLUT_WINDOW_HEIGHT); if( x > y ) { dx = x / (double)y; dy = 1; } else { dx = 1; dy = y / (double)x; } glViewport(0, 0, x, y); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-dx, dx, -dy, dy, 30, 1e6); /* Notice that the up vector of the camera points downwards. This makes the Y coordinates consistent with how we generated the environment map, with smaller Y values near the top of the screen rather than the bottom. Alternatively, we could just generate our environment map upside down, but that makes it cumbersome to debug, hence this tweak. */ gluLookAt(0, 0, -1e4, 0, 0, 0, 0, -1, 0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); /* Draw objects */ /** SetObjectPositions(); **/ for(i = 0; i < OBJECT_COUNT; i++) { for(j = 0; j < 6; j++) { TaraiObject[i][2][j] = ConvertRange(TaraiObject[i][0][6], TaraiObject[i][1][6], TaraiObject[i][0][j], TaraiObject[i][1][j], CurrentTime); } TaraiObject[i][2][6] = CurrentTime; if( CurrentTime >= TaraiObject[i][1][6] ) GenerateNewKeyframe(); glPushMatrix(); glTranslated(TaraiObject[i][2][0], TaraiObject[i][2][1], TaraiObject[i][2][2]); glRotated(TaraiObject[i][2][3], 1, 0, 0); glRotated(TaraiObject[i][2][4], 0, 1, 0); glRotated(TaraiObject[i][2][5], 0, 0, 1); glCallList(TaraiObjectList); glPopMatrix(); } glutSwapBuffers(); glFlush(); } /* Force redraw on clock */ void Animate(void) { glutPostRedisplay(); } /* Force redraw on reshape */ void Reshape(int w, int h) { glutPostRedisplay(); } /* Exit */ void Quit(unsigned char c, int u, int v) { glFlush(); if( CurrentTime - StartTime > 1 ) printf("fps = %f\n", CurrentFrame / (CurrentTime - StartTime)); exit(EXIT_SUCCESS); } int main(int argc, char **argv) { glutInit(&argc, argv); /** Init(); **/ /* Initialize OpenGL + GLUT */ glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutSetWindow(glutCreateWindow("Koyomi")); glutDisplayFunc(Render); glutIdleFunc(Animate); glutReshapeFunc(Reshape); glutKeyboardFunc(Quit); /* Set OpenGL options */ glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glEnable(GL_NORMALIZE); glShadeModel(GL_SMOOTH); /* Initialize objects */ UpdateClock(); srand((unsigned int)CurrentTime); /** InitObj(); **/ TaraiObjectList = glGenLists(1); glNewList(TaraiObjectList, GL_COMPILE); dx = atan2(0, -1) / 24; /* Side rings */ for(i = 0; i < 9 - 1; i++) { glBegin(GL_QUAD_STRIP); for(j = 0; j <= 48; j++) { p = j * dx; glNormal3d(curve[i][2] * cos(p), curve[i][3], curve[i][2] * sin(p)); glVertex3d(curve[i][0] * cos(p), curve[i][1], curve[i][0] * sin(p)); glNormal3d(curve[i + 1][2] * cos(p), curve[i + 1][3], curve[i + 1][2] * sin(p)); glVertex3d(curve[i + 1][0] * cos(p), curve[i + 1][1], curve[i + 1][0] * sin(p)); } glEnd(); } /* Bottom cap */ glBegin(GL_POLYGON); glNormal3d(0, curve[0][3], 0); for(i = 0; i < 48; i++) { p = i * dx; glVertex3d(curve[0][0] * cos(p), curve[0][1], curve[0][0] * sin(p)); } glEnd(); /* Top cap */ glBegin(GL_POLYGON); glNormal3d(0, curve[8][3], 0); for(i = 0; i < 48; i++) { p = i * -dx; glVertex3d(curve[8][0] * cos(p), curve[8][1], curve[8][0] * sin(p)); } glEnd(); glEndList(); /* We could put in a glutSolidTeapot here just for kicks, but due to strange polygon culling, it actually starts to look weird if you stare at it long enough, so no teapot. */ /** InitTexture(); **/ /** GenerateCloudImage(); **/ /* First pass: generate base image */ for(y = 0; y <= 1024; y += 128) { for(x = 0; x <= 1024; x += 128) EnvCloud[y][x] = 0; } for(i = 0; i < 32; i++) { x = ((int)RandomNumber(0, 8)) * 128; y = ((int)RandomNumber(0, 8)) * 128; EnvCloud[y][x] = RandomNumber(0.1, 0.3); } j = 0; SubDivideCloud(128, 0.45 / 8); /* Second pass: tile image */ for(i = 0; i <= 1024; i++) { EnvCloud[1024][i] = EnvCloud[0][i]; EnvCloud[i][1024] = EnvCloud[i][0]; } j = 1; SubDivideCloud(1024, 0.45); /* Third pass: scale image */ for(y = 0; y < 1024; y++) { for(x = 0; x < 1024; x++) EnvCloud[y][x] *= 255; } /** GenerateTextureMap(); **/ for(y = 0; y < 512; y++) { dy = ConvertRange(0, 511, 1, -1, y); for(x = 0; x < 512; x++) { dx = ConvertRange(0, 511, -1, 1, x); r2 = dx * dx + dy * dy; ly = sqrt((1 - dx * dx) / 2); if( dy >= ly ) { /* Sky */ ty = 1; SetTexturePixel(); } else if( dy <= -ly ) { /* Ground */ ty = -1; SetTexturePixel(); } else { /* Bits in between */ if( r2 < 1 ) { ty = dy / sqrt(1 - r2); if( ty >= -1 && ty <= 1 ) SetTexturePixel(); } } /* Modulate in cloud texture if we are on the sphere */ if( r2 < 1 ) { p = EnvCloud[ModulusRange(dx / sqrt(1 - r2))] [ModulusRange(dy / sqrt(1 - r2))]; if( p > 0 ) { for(i = 0; i < 3; i++) { j = (int)TextureImage[y][x][i] + (int)p; TextureImage[y][x][i] = (unsigned char)(j > 255 ? 255 : j); } } } } } glEnable(GL_TEXTURE_2D); glGenTextures(1, &TextureObject); glBindTexture(GL_TEXTURE_2D, TextureObject); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); StartTime = CurrentTime; CurrentFrame = 0; /** InitObjPositions(); **/ for(i = 0; i < OBJECT_COUNT; i++) { for(j = 0; j < 6; j++) TaraiObject[i][2][j] = RandomNumber(-360, 360); TaraiObject[i][2][7] = TaraiObject[i][2][6] = CurrentTime; GenerateNewKeyframe(); } glutMainLoop(); return 0; }