Klokke
Prosjektet er bygget rundt en klasse: TreDClock . Det er dessuten laget en klasse for å holde styr på materialer (Material), en klasse for å håndtere 3D-tekst (Fonts). Alle klassene er lagt på en fil, Program.cs. Programmet benytter seg dessuten av en dll-fil som regner om tiden til klokkevinkler.
TreDClock
Følgende felter er definert i klassen:
private static IntPtr hDC; // Device Context private static IntPtr hRC; // Rendering Context private static Form form; // The Form we will draw in private static bool[] keys = new bool[256]; // Reading Keyboard status private static bool active = true; // Window active or not private static bool done = false; // Controlling main loop // describing the clock private static float radius = 1.0f; private static float thickness = 0.1f; private static float marker_radius=0.05f; private static float Hour_Arm_Length = 0.6f; private static float Hour_Arm_Radius = 0.02f; private static float Minute_Arm_Length = 0.9f; private static float Minute_Arm_Radius = 0.02f; private static float Second_Arm_Length = 0.9f; private static float Second_Arm_Radius = 0.005f; // A font handling object private static Fonts myFont; // mousing around private static bool MouseIsDown = false; private static Point Last_Point = new Point(0, 0); private static float AngleX = 0.0f; private static float AngleY = 0.0f;
Konstruktøren tar seg av to oppgaver. Først preparerer den formen for OpenGL, og deretter deklarerer vi våre callback-metoder for å plukke opp events.
public TreDClock() { // We must set up some attributes for this form to make it usefull for OpenGL this.CreateParams.ClassStyle = this.CreateParams.ClassStyle | User.CS_HREDRAW | User.CS_VREDRAW | // Redraw on Size User.CS_OWNDC; // Own DC for this window. this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); // Dont erase Form background this.SetStyle(ControlStyles.DoubleBuffer, true); // Buffer control this.SetStyle(ControlStyles.Opaque, true); // Dont draw Form background this.SetStyle(ControlStyles.ResizeRedraw, true); // Redraw on Resize this.SetStyle(ControlStyles.UserPaint, true); // We will paint ourselves // connect our methods to events (delegation) this.Activated += new EventHandler(this.Form_Activated); this.Closing += new CancelEventHandler(this.Form_Closing); this.Deactivate += new EventHandler(this.Form_Deactivate); this.KeyDown += new KeyEventHandler(this.Form_KeyDown); this.KeyUp += new KeyEventHandler(this.Form_KeyUp); this.Resize += new EventHandler(this.Form_Resize); this.MouseDown += new MouseEventHandler(this.Form_MouseDown); this.MouseMove += new MouseEventHandler(this.Form_MouseMove); this.MouseUp += new MouseEventHandler(this.Form_MouseUp); }
De tre "standard" rutinene for å initialisere OpenGL, tegne scenen, og handtere resize av vinduet er slik:
private static bool InitGL() { // Once in a lifetime jobs // set up light float[] ambient = { 0.5f, 0.5f, 0.5f, 1.0f }; float[] diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }; float[] position = { 0.0f, 0.0f, 10.0f, 0.0f }; Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, ambient); Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, diffuse); Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_POSITION, position); Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_AMBIENT, ambient); Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_DIFFUSE, diffuse); // light1 is positioned in draw // enable light Gl.glEnable(Gl.GL_LIGHTING); Gl.glEnable(Gl.GL_LIGHT0); Gl.glEnable(Gl.GL_LIGHT1); // depth sorting Gl.glEnable(Gl.GL_DEPTH_TEST); Gl.glDepthFunc(Gl.GL_LESS); // background Gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // force unit normals Gl.glEnable(Gl.GL_NORMALIZE); // build font myFont =new Fonts(hDC); myFont.BuildFont(); return true; }
private static bool DrawGLScene() { Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT | Gl.GL_ACCUM_BUFFER_BIT); Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glLoadIdentity(); Glu.gluLookAt(0, 0, 5, // eyepos 0, 0, 0, // look at 0, 1, 0);// up-vector Gl.glRotatef(90.0f, 0.0f, 0.0f, 1.0f); Gl.glRotatef(AngleY, 1.0f, 0.0f, 0.0f); Gl.glRotatef(AngleX, 0.0f, 0.0f, 1.0f); AngleY += 0.1f; Glu.GLUquadric q; q = Glu.gluNewQuadric(); // note this: we make no C#-object // clock laying with its back on xy-plane // draw body Material.setClockBodyMaterial(); Gl.glPushMatrix(); Glu.gluQuadricDrawStyle(q, Glu.GLU_FILL); Glu.gluCylinder(q, radius, radius, thickness, 100, 100); Glu.gluDisk(q, 0.0f, radius, 50, 5); Gl.glTranslatef(0.0f, 0.0f, thickness); Glu.gluDisk(q, 0.0f, radius, 50, 5); Glu.gluCylinder(q, radius / 10.0f, radius / 10.0f, thickness, 20, 20); Gl.glTranslatef(0.0f, 0.0f, thickness); Material.setClockMarkerMaterial(); Glu.gluDisk(q, 0.0f, radius / 10.0f, 50, 5); Gl.glTranslatef(0.0f, 0.0f, -thickness); //markers Material.setClockMarkerMaterial(); float angle=0.0f; while (angle < 361.0f) { Gl.glPushMatrix(); Gl.glRotatef(angle, 0.0f, 0.0f, 1.0f); Gl.glTranslatef(radius - marker_radius - 0.01f, 0.0f, 0.0f); Glu.gluSphere(q, marker_radius, 10, 10); Gl.glPopMatrix(); angle += 30.0f; } Gl.glPopMatrix(); // arms Material.setClockArmsMaterial(); // draw Seconds arm Gl.glPushMatrix(); Gl.glTranslatef(0.0f, 0.0f,thickness + Second_Arm_Radius); Gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f); Gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); Gl.glRotatef(-clocklib.TimeAngle.SecondAngleNow, 1.0f, 0.0f, 0.0f); Glu.gluCylinder(q, Second_Arm_Radius, Second_Arm_Radius, Second_Arm_Length, 5,50); Gl.glPopMatrix(); // draw Minutes arm Gl.glPushMatrix(); Gl.glTranslatef(0.0f,0.0f,thickness + 2 * Second_Arm_Radius + Minute_Arm_Radius); Gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f); Gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); Gl.glRotatef(-clocklib.TimeAngle.MinuteAngleNow, 1.0f, 0.0f, 0.0f); Glu.gluCylinder(q, Minute_Arm_Radius, Minute_Arm_Radius, Minute_Arm_Length, 5, 50); Gl.glPopMatrix(); // draw Hours arm Gl.glPushMatrix(); Gl.glTranslatef(0.0f, 0.0f, thickness + 2 * (Second_Arm_Radius + Minute_Arm_Radius) + Hour_Arm_Radius); Gl.glRotatef(90.0f, 0.0f, 1.0f, 0.0f); Gl.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); Gl.glRotatef(-clocklib.TimeAngle.HourAngleNow, 1.0f, 0.0f, 0.0f); Glu.gluCylinder(q, Hour_Arm_Radius, Hour_Arm_Radius, Hour_Arm_Length, 5, 50); Gl.glPopMatrix(); // backside Gl.glPushMatrix(); Gl.glRotatef(180.0f, 0.0f, 0.0f, 1.0f); Gl.glRotatef(180.0f, 0.0f, 1.0f, 0.0f); Gl.glRotatef(-90.0f, 0.0f, 0.0f, 1.0f); Gl.glTranslatef( 0.0f, 0.0f,-0.1f);// trial and error Gl.glTranslatef(-0.4f*radius, 0.0f, 0.3f); Gl.glScalef(0.3f, 0.3f, 1.0f); float[] position = { 0.0f, 0.0f, 2.0f, 1.0f }; Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_POSITION, position); Gl.glLightf(Gl.GL_LIGHT1, Gl.GL_SPOT_CUTOFF, 80.0f); Gl.glLighti(Gl.GL_LIGHT1, Gl.GL_SPOT_EXPONENT, 25); myFont.glPrint(clocklib.TimeAngle.DigitalFormNow); Gl.glPopMatrix(); Glu.gluDeleteQuadric(q); Gdi.SwapBuffers(hDC); return true; }
private static void ReSizeGLScene(int width, int height) { // dont want a divide by 0 if (height == 0) height = 1; Gl.glMatrixMode(Gl.GL_PROJECTION); Gl.glLoadIdentity(); Gl.glViewport(0, 0, width, height); Glu.gluPerspective(30.0f, (double)width / height, 0.1f, 70.0f); Gl.glMatrixMode(Gl.GL_MODELVIEW); }
Våre egne metoder for å handtere events er rett fram:
private void Form_Activated(object sender, EventArgs e) { active = true; // Program Is Active } private void Form_Closing(object sender, CancelEventArgs e) { // Send A Quit Message, will exit event loop done = true; } private void Form_Deactivate(object sender, EventArgs e) { // Program is not active anymore active = false; } private void Form_KeyDown(object sender, KeyEventArgs e) { // Key pressed, Mark it true keys[e.KeyValue] = true; } private void Form_KeyUp(object sender, KeyEventArgs e) { // Key released, Mark it false keys[e.KeyValue] = false; } private void Form_Resize(object sender, EventArgs e) { ReSizeGLScene(form.Width, form.Height); } private void Form_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { MouseIsDown = true; Last_Point.X = e.X; Last_Point.Y = e.Y; } } private void Form_MouseMove(object sender, MouseEventArgs e) { if ((e.Button == MouseButtons.Left)&&(MouseIsDown)) { AngleX += 90.0f * ((float)(e.X - Last_Point.X) / this.Width); AngleY += 90.0f * ((float)(e.Y - Last_Point.Y) / this.Height); Last_Point.X = e.X; Last_Point.Y = e.Y; this.Invalidate(); } } private void Form_MouseUp(object sender, MouseEventArgs e) { MouseIsDown = false; }
Mainmetoden som drar det hele i gang er slik:
[STAThread] public static void Main(string[] commandLineArguments) { // Create OpenGL Window if (!CreateGLWindow("3D Clock", 300, 300, 16)) return; // we want to control the event-loop ourselves while (!done) { // Application process events and use our delegated methods Application.DoEvents(); // What have happened, have our methods changed any flags ? // We will quit when escape or window close if ((active && (form != null) && !DrawGLScene()) || keys[(int)Keys.Escape]) { // A signal to quit done = true; } else { // No quitting so we redraw by swapping buffers (double buffering is set) Gdi.SwapBuffers(hDC); } } // We have received a quit-signal and we shutdown myFont.KillFont(); KillGLWindow(); return; }
Det er to metoder vi hopper over, CreateGLWindow og KillGLWindow. Det er de som setter opp Formens Device Context mot OpenGls Rendering Context, og tar dette ned igjen. Disse rutinene er beskrevet i modulen C#/.Net, og du kan inspisere dem dersom du popper opp hele fila, se nedenfor.
Material
Denne klassen er laget bare for å rydde i koden. Koden er forhåpentlig selvforklarende:
public static class Material : Object { public static void setClockBodyMaterial(){ // matted brass float[] mat_ambient={0.329412f,0.223529f,0.027451f}; float[] mat_diffuse={0.780392f,0.568627f,0.113725f }; Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT, mat_ambient); Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_DIFFUSE, mat_diffuse); } public static void setClockMarkerMaterial() { float[] mat_ambient ={ 0.9f, 0.1f, 0.0f, 1.0f }; float[] mat_diffuse ={ 1.0f, 0.0f, 0.1f, 1.0f }; Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT, mat_ambient); Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_DIFFUSE, mat_diffuse); } public static void setClockArmsMaterial() { float[] mat_ambient ={ 0.0f, 0.1f, 0.0f, 1.0f }; float[] mat_diffuse ={ 0.0f, 0.0f, 0.1f, 1.0f }; Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_AMBIENT, mat_ambient); Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_DIFFUSE, mat_diffuse); } }
Fonts
Siden vi opererer i MS Windows kan vi uten betenkligheter bruke 3D-tekst slik som vi har tilgang til under Windows. Dette er også drøftet i modulen: Tekst i Windows. Klassen nedenfor har tre metoder, en for å sette opp en font, en for å ta fonten ned, og en for å produsere tekst. Merk at konstruktøren setter en lokal variabel som angir hvilken Device Context vi arbeider med. Dette er den samme Device Contexten som er bundet til OpenGLs rendering Context.
public class Fonts : Object { private static IntPtr hDC; private static int fontbase; private static Gdi.GLYPHMETRICSFLOAT[] gmf = new Gdi.GLYPHMETRICSFLOAT[256]; public Fonts( IntPtr pDC){ hDC = pDC; } public void BuildFont() { IntPtr font; fontbase = Gl.glGenLists(256); font = Gdi.CreateFont(-12, 0, 0, 0, Gdi.FW_BOLD, false, false, false, Gdi.ANSI_CHARSET, Gdi.OUT_TT_PRECIS, Gdi.CLIP_DEFAULT_PRECIS, Gdi.ANTIALIASED_QUALITY, Gdi.FF_DONTCARE | Gdi.DEFAULT_PITCH, "Comic Sans MS"); Gdi.SelectObject(hDC, font); Wgl.wglUseFontOutlines( hDC, 0, 255, fontbase, 0, 0.2f, Wgl.WGL_FONT_POLYGONS, gmf); } public void KillFont() { Gl.glDeleteLists(fontbase, 256); } public void glPrint(string text) { // use culling to save time (letters are closed) Gl.glEnable(Gl.GL_NORMALIZE); Gl.glCullFace(Gl.GL_BACK); Gl.glEnable(Gl.GL_CULL_FACE); Gl.glPushMatrix(); if (text == null || text.Length == 0) return; Gl.glPushAttrib(Gl.GL_LIST_BIT); Gl.glListBase(fontbase); byte[] textbytes = new byte[text.Length]; for (int i = 0; i < text.Length; i++) textbytes[i] = (byte)text[i]; Gl.glCallLists(text.Length, Gl.GL_UNSIGNED_BYTE, textbytes); Gl.glPopAttrib(); Gl.glPopMatrix(); Gl.glDisable(Gl.GL_CULL_FACE); } }
DLL-clocklib
Denne fila er svært enkel og er sitert i sin helhet nedenfor:
using System; using System.Collections.Generic; using System.Text; namespace clocklib { #region Class Documentation /// <summary> /// TimeAngle /// </summary> /// <remarks> /// <para> /// Implements methods to convert /// current time to angles and digital format. /// </para> /// <para> /// Seconds and minutes are treated in unit steps, /// while hours are treated with minute-increment within the hour. /// </para> /// </remarks> #endregion Class Documentation public class TimeAngle { private static float sec_angle(DateTime dt){ return -(360.0f * ((float)dt.Second / 60.0f)-90.0f); } private static float min_angle(DateTime dt) { return -(360.0f * ((float)dt.Minute / 60.0f) - 90.0f); } private static float hour_angle(DateTime dt) { return -(360.0f / 12.0f * ((float)dt.Hour + ((float)dt.Minute) / 60.0f) - 90.0f); } /// <summary> /// Secondspart of current time converted to degrees on an analog clock /// </summary> public static float SecondAngleNow { get{ return sec_angle(DateTime.UtcNow.ToLocalTime()); } } /// <summary> /// Secondspart of given time converted to degrees on an analog clock /// </summary> public static float SecondAngle(DateTime dt) { return sec_angle(dt); } /// <summary> /// Minutespart of current time converted to degrees on an analog clock /// </summary> public static float MinuteAngleNow { get { return min_angle(DateTime.UtcNow.ToLocalTime()); } } /// <summary> /// Minutespart of given time converted to degrees on an analog clock /// </summary> public static float MinuteAngle(DateTime dt) { return min_angle(dt); } /// <summary> /// Hoursspart of current time converted to degrees on an analog clock /// </summary> public static float HourAngleNow { get { return hour_angle(DateTime.UtcNow.ToLocalTime()); } } /// <summary> /// Hoursspart of given time converted to degrees on an analog clock /// </summary> public static float HourAngle(DateTime dt) { return hour_angle(dt); } ///<summary> /// All angles at current time as ref variables ///</summary> public static void AllAnglesNow(ref float sAngle,ref float mAngle,ref float hAngle) { sAngle=HourAngleNow; mAngle=MinuteAngleNow; hAngle=HourAngleNow; } ///<summary> /// All angles at given time as ref variables ///</summary> public static void AllAnglesNow(DateTime dt,ref float sAngle, ref float mAngle, ref float hAngle) { sAngle = HourAngle(dt); mAngle = MinuteAngle(dt); hAngle = HourAngle(dt); } /// <summary> /// Current time as hh:mm:ss /// </summary> public static String DigitalFormNow {get{ return DateTime.UtcNow.ToLocalTime().ToLongTimeString();}} /// <summary> /// Given time as hh:mm:ss /// </summary> public static String DigitalForm(DateTime dt) {return dt.ToLongTimeString();} } }
Den kompilerte versjonen av denne, clocklib.dll, må koples til i Visual Studio dersom du skal eksperimentere med dette klokkeprosjektet. Dette gjøres ved å høyreklikke på References i Solution Explorer-vinduet i Visual Studio og velge "Add Reference..". Det kommer da opp en dialogboks som gir deg noen valg. Du kan enten velge fra lista over assemblies som er registrerte som system globale eller i dette tilfellet må du browse til den katalogen do har plassert clocklib.dll.
Alle de andre klassene finner på en fil her: Program.cs
- DLL-prosjektet (Visual Studio/.Net): clocklib.zip
- OpenGL-prosjektet (Visual Studio/.Net): Clock.zip