3D-tekst
Spotlight
TAO
C#
Grafikk
Børre Stenseth
Å tegne:>Klokke

Klokke

Hva

I denne modulen tegner vi en 3D-klokke, analog på forsiden og digital på baksiden. Prosjektet er bygget som et console-prosjekt i Visual Studio 2005, .Net versjon2.

clock

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

[1]
Referanser
  1. Tao FrameworkSourceForge.netBindings to facilitate cross-platform development utilizing the .NET platformsourceforge.net/projects/taoframework/14-03-2010
Vedlikehold

B.Stenseth, desember 2005

Å tegne:>Klokke
til toppen