Page 1 of 3 123 LastLast
Results 1 to 10 of 25

Thread: A Simple OpenGL Framework - Your input required...

  1. #1

    A Simple OpenGL Framework - Your input required...

    I plan to try and get back into 3D programming with OpenGL. As such I would like to create a simple OpenGL framework for games, based in part on things I read in the OpenGL Game programming book a few years ago. So over the next few days, I plan to post various classes that I plan to use in the future and that will become part of JEDI-SDL, so most will have an SDL slant.

    My hope is that with everyone's input we can create a set of classes that are clean, documented and that everyone will find usefull and easy to use, not just me. So I'm looking for comments about what is wrong, what could be done better, what is useless.

    Here is my first Vector class...
    [pascal]
    unit vector;

    interface

    type
    TScalar = single;

    TVector = class( TObject )
    public
    x : TScalar;
    y : TScalar;
    z : TScalar; // x,y,z coordinates

    constructor Create( a : TScalar = 0; b : TScalar = 0; c : TScalar = 0 ); overload;
    constructor Create( const vec : TVector ); overload;


    // vector assignment
    function Assign( const vec : TVector ) : TVector;

    // vector equality
    function IsEqual( const vec : TVector ) : Boolean;

    // vector inequality
    function IsNotEqual( const vec : TVector ) : Boolean;

    // vector add
    function Add( const vec : TVector ) : TVector;

    // vector Increment
    function Inc( const vec : TVector ) : TVector;

    // vector subtraction
    function Subtract( const vec : TVector ) : TVector;

    // vector Decrement
    function Dec( const vec : TVector ) : TVector;

    // vector negation
    function Negative : TVector;

    // vector Positivisation
    function Positive : TVector;

    // Scale
    function Scale( s : TScalar ) : TVector;

    // Multiply
    function Multiply( vec : TVector ) : TVector;

    // Divide
    function Divide( vec : TVector ) : TVector;

    // cross product
    function CrossProduct( const vec : TVector ) : TVector;

    // dot product
    function DotProduct( const vec : TVector ) : TScalar;


    // Interpolate
    function Interpolate( const vec : TVector; Amount : TScalar ) : TVector;

    // length of vector
    function Length : TScalar;

    // Set the length of vector
    function SetLength( LengthLimit : TScalar ) : TVector;

    // return the unit vector
    function UnitVector : TVector;

    // normalize this vector
    procedure Normalize;

    // return angle between two vectors
    function Angle( const vec : TVector ) : TScalar;

    // reflect this vector off surface with normal vector
    function Reflection( const normal : TVector ) : TVector;
    end;

    implementation

    function TVector.Assign( const vec : TVector ) : TVector;
    begin
    x := vec.x;
    y := vec.y;
    z := vec.z;

    result := self;
    end;

    constructor TVector.Create( a : TScalar = 0; b : TScalar = 0; c : TScalar = 0 );
    begin
    x := a;
    y := b;
    z := c;
    end;

    constructor TVector.Create( const vec : TVector );
    begin
    x := vec.x;
    y := vec.y;
    z := vec.z;
    end;

    function TVector.IsEqual( const vec : TVector ) : Boolean;
    begin
    result := ( ( x = vec.x ) and ( y = vec.y ) and ( z = vec.z ) );
    end;

    function TVector.IsNotEqual( const vec : TVector ) : Boolean;
    begin
    result := not IsEqual( vec );
    end;

    function TVector.Add( const vec : TVector ) : TVector;
    begin
    result := TVector.Create( x + vec.x, y + vec.y, z + vec.z );
    end;

    function TVector.Subtract( const vec : TVector ) : TVector;
    begin
    result := TVector.Create( x - vec.x, y - vec.y, z - vec.z );
    end;

    function TVector.Negative : TVector;
    begin
    result := TVector.Create( -x, -y, -z );
    end;

    function TVector.Scale( s : TScalar ) : TVector;
    begin
    x := x * s;
    y := y * s;
    z := z * s;

    result := self;
    end;

    function TVector.Multiply( vec : TVector ) : TVector;
    begin
    x := x * vec.x;
    y := y * vec.y;
    z := z * vec.z;

    result := self;
    end;

    function TVector.CrossProduct( const vec : TVector ) : TVector;
    begin
    result := TVector.Create( y * vec.z - z * vec.y, z * vec.x - x * vec.z, x * vec.y - y * vec.x );
    end;

    function TVector.DotProduct( const vec : TVector ) : TScalar;
    begin
    result := x * vec.x + y * vec.y + z * vec.z;
    end;

    function TVector.Interpolate( const vec : TVector; Amount : TScalar ) : TVector;
    begin
    X := X + ( vec.X - X ) * Amount;
    Y := Y + ( vec.Y - Y ) * Amount;
    Z := Z + ( vec.Z - Z ) * Amount;
    result := self;
    end;

    function TVector.Length : TScalar;
    begin
    result := sqrt( ( x * x + y * y + z * z ) );
    end;

    function TVector.UnitVector : TVector;
    begin
    x := x / Length;
    y := y / Length;
    z := z / Length;

    result := self;
    end;

    procedure TVector.Normalize;
    var
    ScaleValue, Len : Single;
    begin
    Len := Length;
    if Len = 0.0 then
    Exit;

    ScaleValue := 1.0 / Len;

    Scale( ScaleValue );
    end;

    function TVector.Angle( const vec : TVector ) : TScalar;
    begin
    result := VectArcCos( self.DotProduct( vec ) );
    end;

    function TVector.Reflection( const normal : TVector ) : TVector;
    var
    vec : TVector;
    begin
    vec := TVector.Create( self.SetLength( 1 ) ); // normalize this vector
    result := vec.Subtract( normal.Scale( 2.0 * ( vec.DotProduct( normal ) ) ) ).Scale( Length );
    end;

    function TVector.Divide( vec : TVector ) : TVector;
    begin
    x := x / vec.x;
    y := y / vec.y;
    z := z / vec.z;

    result := self;
    end;

    function TVector.Positive : TVector;
    begin
    result := TVector.Create( +x, +y, +z );
    end;

    function TVector.SetLength( LengthLimit : TScalar ) : TVector;
    begin
    result := Scale( LengthLimit / Length );
    end;

    function TVector.Dec( const vec : TVector ) : TVector;
    begin
    x := x - vec.x;
    y := y - vec.y;
    z := z - vec.z;

    result := self;
    end;

    function TVector.Inc( const vec : TVector ) : TVector;
    begin
    x := x + vec.x;
    y := y + vec.y;
    z := z + vec.z;

    result := self;
    end;
    [/pascal]
    <br /><br />There are a lot of people who are dead while they are still alive. I want to be alive until the day I die.<br />-= Paulo Coelho =-

  2. #2

    A Simple OpenGL Framework - Your input required...

    I had a quick look and found one error. I also have a question.

    First the question. Why do you use: TScalar = single;
    and then everywhere x : TScalar; instead of x : single;. IMO the last makes it more readable.

    I've tried to compile it but it gave an error at line 198 undeclared identifier VectArcCos.

  3. #3

    A Simple OpenGL Framework - Your input required...

    Traveler, own types are a good thing for cross platform stuff, because system types might be of different size on different platforms, and in case and you want to use different precision you only have to change one line.

  4. #4

    A Simple OpenGL Framework - Your input required...

    Good point there. Thanks for clearing that up!

  5. #5

    A Simple OpenGL Framework - Your input required...

    Using

    TScalar = Single;

    means that if he decides to change to Doubles later on, he only has to change it in one spot, instead of refactoring the change everywhere throughout his code.

    savage, do you think that using a class is the best idea for a vector? Each vector that you create will have a four byte overhead inherited from TObject (TObject.InstanceSize = 4). This overhead is because some methods of TObject are declared virtual and therefore every instance of TObject has a pointer to the virtual method table (VMT). Therefore each instance of your TVector class will be 16 bytes, whereas a record with the same members will be 12 bytes.

    Another disadvantage of classes is that you must allocate each one individually. Unless you override the NewInstance and FreeInstance methods of TObject to grab memory from a pre-allocated memory pool. But then, think about what is happening here. If you are allocating a pool of 200 vectors, then you must call Create on each instance, which in turn calls NewInstance (a virtual method call, ie. slower), which in turn calls InitInstance, and other method calls such as InstanceSize. That is at least 200 calls to Create, 200 virtual calls to NewInstance, and 200 calls to InitInstance among others. Then when you free them you have to call Free, which calls Destroy (another virtual call), which calls CleanupInstance and FreeInstance, which calls InstanceSize again. That's a lot of overhead behind the scenes for such a simple class.

    A lot of your methods create a new TVector instance and return that. Unless you are going to be very fanatical about remembering to free all those returned instances in the caller function, you are going to end up with a huge amount of lost memory.

    I am writing a Vector unit as well, but I am using records and standard procedures/functions to manipulate those records.

    The other advantage of using records is that you can block allocate a pool of them and initialize them all to zero with one call to FillChar.

    I'm also using operator overloads in FreePascal so I can do something like

    VectorC := VectorA + VectorB;

    Now for some comments on the code.

    [pascal]function TVector.Length : TScalar;
    begin
    result := sqrt( ( x * x + y * y + z * z ) );
    end; [/pascal]

    You should also provide a LengthSquare() method. A lot of algorithms can use the square of the vector length instead of the actual vector length. This saves performing an expensive square root operation.

    [pascal]function TVector.UnitVector : TVector;
    begin
    x := x / Length;
    y := y / Length;
    z := z / Length;

    result := self;
    end; [/pascal]

    Calculate once and store. Here, you are calling Length three times. But there is also a bug here. The first call to Length will give you the actual length. The second call will be wrong because you have already modified the x value. This will affect the second and third calls to Length.

    [pascal]procedure TVector.Normalize;
    var
    ScaleValue, Len : Single;
    begin
    Len := Length;
    if Len = 0.0 then
    Exit;

    ScaleValue := 1.0 / Len;

    Scale( ScaleValue );
    end; [/pascal]

    This is a better implementation. The previous method (UnitVector) was trying to do this, but incorrectly. This method also shows another optimization. A multiplication is quicker than a divide, so if you are going to be dividing by the same value, invert it and multiply instead.

    [pascal]function TVector.Reflection( const normal : TVector ) : TVector;
    var
    vec : TVector;
    begin
    vec := TVector.Create( self.SetLength( 1 ) ); // normalize this vector
    result := vec.Subtract( normal.Scale( 2.0 * ( vec.DotProduct( normal ) ) ) ).Scale( Length );
    end; [/pascal]

    Here is an example of some memory leaks. vec is leaking. The operation of creating vec is a bit strange too. Basically what you are doing is getting a normalized version of the vector in a long roundabout way. The call to SetLength(1) is the same as calling Normalize(). Then you copy the normalized vector to vec. There is a bug in this function however in that you have normalized the vector before you calculate the reflection, so the call to Length() in the second line is always going to return 1.0.

    Something for you to think about.

  6. #6

    A Simple OpenGL Framework - Your input required...

    Hi Dom

    This would be a nice idea, I've spent ages porting various 3D utility functions to suit my own purposes, one that ships with JEDI-SDL would be nice.

    the point about using classes is well founded I think. But using methods on vectors is nicer than using functions (which I have been using). Operator overloads are nice but are only supported by FPC, so I think they should be avoided.

    If we put together a nice memory manager that all the objects in the framework can link into we could make a really nice and quick object based frame work. I have some code on a Frame Based memory manager (see 3d programming gems) which works well, I'll see if I can upload some examples to the JEDI list for you too look at. It might help.

    PS Dont forget you have the a3dngine SDL port to look at as well.

    Dean
    <A HREF="http://www.myhpf.co.uk/banner.asp?friend=139328">
    <br /><IMG SRC="http://www.myhpf.co.uk/banners/60x468.gif" BORDER="0">
    <br /></A>

  7. #7

    A Simple OpenGL Framework - Your input required...

    Hi Sly,
    The decision to go with classes was a conscious one. I am aware that records are more compact and quicker using traditional methods, but I wanted to use classes for clarity and not speed. The idea being that once their game/engine is up and running they can look into where the bottlenecks are then remove them, ie rewrite their Vector handling for example.

    Just on the class size/speed issue, would it be better to use Object Pascal's "object" keyword instead for these kind of objects. Does anyone have any info on how much better this performs over classes?

    Thanks again for all the comments on the code, they have proved invaluable as always. Nice work spotting that UnitVector bug, I completely overlooked it and was wondering why one of my demos was misbehaving.

    In the case of returning a new instance of a class, what would be a better way of implementing this? Should it just change the current vector, thus leaving it up to the developer using the class to handle storing/restoring the vector as needed?

    Is anything missing, anything else that is needed when working with vectors?
    <br /><br />There are a lot of people who are dead while they are still alive. I want to be alive until the day I die.<br />-= Paulo Coelho =-

  8. #8

    A Simple OpenGL Framework - Your input required...

    Hi,

    I agree on not using class for this sort of thing (since it's basically a math unit).

    That said.

    Quote Originally Posted by savage
    In the case of returning a new instance of a class, what would be a better way of implementing this? Should it just change the current vector, thus leaving it up to the developer using the class to handle storing/restoring the vector as needed?
    How about doing the math on the current class and instead introducing a clone method.
    Eg.

    OriginalVector.Add( AVector );

    would perfom transformation to the original and

    TransformedVector := OriginalVector.Clone.Add( AVector );

    Would return a newly created vector.

    Cheers,
    Balaras
    I've been trying for some time to develop a lifestyle that doesn't require my presence. - G.B. Trudeau

  9. #9

    A Simple OpenGL Framework - Your input required...

    Quote Originally Posted by savage
    Hi Sly,
    The decision to go with classes was a conscious one. I am aware that records are more compact and quicker using traditional methods, but I wanted to use classes for clarity and not speed. The idea being that once their game/engine is up and running they can look into where the bottlenecks are then remove them, ie rewrite their Vector handling for example.
    I think having vectors as a class will reduce clarity as well as speed. You will spend more time managing the creation and destruction of TVector instances than using the vectors to do useful things, let alone tracking down lost memory because you forgot to free some vectors somewhere.

    Another downside to using classes is memory fragmentation. You will be doing potentially tens or hundreds of thousands of tiny memory allocations and deallocations that is going to cause heartache for the standard Borland memory manager. Some of the replacement memory managers at http://www.fastcode.dk will handle it better, but that's a dependency on a third-party library.

    Just on the class size/speed issue, would it be better to use Object Pascal's "object" keyword instead for these kind of objects. Does anyone have any info on how much better this performs over classes?
    Using 'object' instead of 'class' could eliminate a lot of the shortcomings of classes that I mentioned. From the Delphi help: "Since object types do not descend from TObject, they provide no built-in constructors, destructors, or other methods. You can create instances of an object type using the New procedure and destroy them with the Dispose procedure, or you can simply declare variables of an object type, just as you would with records." It also mentions that 'object' is supported for backwards compatibility only and should not be used.

    In the case of returning a new instance of a class, what would be a better way of implementing this? Should it just change the current vector, thus leaving it up to the developer using the class to handle storing/restoring the vector as needed?
    Have the user supply a vector to populate. That way it acts as a reminder that the user has to destroy it because they created it.

    Is anything missing, anything else that is needed when working with vectors?
    Your suite of functions seems fairly comprehensive at this stage.

  10. #10

    A Simple OpenGL Framework - Your input required...

    Quote Originally Posted by technomage
    the point about using classes is well founded I think. But using methods on vectors is nicer than using functions (which I have been using). Operator overloads are nice but are only supported by FPC, so I think they should be avoided.
    That is why my operator overloads are surrounded by {$IFDEF FPC}. I also supply standard functions that do the same task.
    Code:
    &#123;$IFDEF FPC&#125;
    operator + &#40;const v1&#58; TVector; const v2&#58; TVector&#41; v3&#58; TVector;
    operator - &#40;const v1&#58; TVector; const v2&#58; TVector&#41; v3&#58; TVector;
    operator * &#40;const v1&#58; TVector; const v2&#58; TVector&#41; v3&#58; TVector;
    operator * &#40;const v1&#58; TVector; s&#58; TScalar&#41; v3&#58; TVector;
    operator / &#40;const v1&#58; TVector; s&#58; TScalar&#41; v3&#58; TVector;
    operator = &#40;const v1&#58; TVector; const v2&#58; TVector&#41; b&#58; Boolean;
    &#123;$ENDIF&#125;  // FPC
    
    function VectorAdd&#40;const v1&#58; TVector; const v2&#58; TVector&#41;&#58; TVector;
    function VectorSubtract&#40;const v1&#58; TVector; const v2&#58; TVector&#41;&#58; TVector;
    function VectorMultiply&#40;const v1&#58; TVector; const v2&#58; TVector&#41;&#58; TVector; overload;
    function VectorMultiply&#40;const v1&#58; TVector; s&#58; TScalar&#41;&#58; TVector; overload;
    function VectorDivide&#40;const v1&#58; TVector; s&#58; TScalar&#41;&#58; TVector;
    function VectorEqual&#40;const v1&#58; TVector; const v2&#58; TVector&#41;&#58; Boolean;
    [quote]If we put together a nice memory manager that all the objects in the framework can ]

    See http://www.fastcode.dk for a memory manager challenge that has some good replacement memory managers. FastMM4 and BucketMM are apparently very good.

Page 1 of 3 123 LastLast

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •