PDA

View Full Version : A Simple OpenGL Framework - Your input required...

savage
05-07-2005, 08:52 AM
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...

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;

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;

Traveler
05-07-2005, 09:56 AM
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.

Paulius
05-07-2005, 10:28 AM
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.

Traveler
05-07-2005, 10:36 AM
Good point there. Thanks for clearing that up!

Sly
05-07-2005, 11:03 AM
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.

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

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.

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

result := self;
end;

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.

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

ScaleValue := 1.0 / Len;

Scale( ScaleValue );
end;

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.

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;

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.

technomage
05-07-2005, 12:44 PM
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

savage
05-07-2005, 12:47 PM
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?

Balaras
05-07-2005, 01:10 PM
Hi,

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

That said.

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.

would perfom transformation to the original and

Would return a newly created vector.

Cheers,
Balaras

Sly
05-07-2005, 01:16 PM
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.

Sly
05-07-2005, 01:22 PM
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.
&#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.

savage
06-07-2005, 05:44 PM
So is this better??

unit vector;

interface

type
TScalar = single;

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

constructor Create( aX : TScalar = 0; aY : TScalar = 0; aZ : TScalar = 0 ); overload;
constructor Create( const vec : TVector ); overload;

// vector assignment
function Assign( const vec : TVector ) : TVector; overload;
function Assign( aX : TScalar = 0; aY : TScalar = 0; aZ : TScalar = 0 ) : TVector; overload;

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

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

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

// vector subtraction
procedure Subtract( const vec : TVector; var aSubtractedVector : TVector );

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

// vector negation
procedure Negative( var aNegativeVector : TVector );

// vector Positivisation
procedure Positive( var aPositiveVector : TVector );

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

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

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

// cross product
procedure CrossProduct( const vec : TVector; var aCrossProductedVector : TVector );

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

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

// length of vector
function Length : TScalar;

// square of the vector length
function LengthSquare : 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
procedure Reflection( const normal : TVector; var aReflectionVector : TVector );
{
const CVector vec(*this | 1); // normalize this vector
result := (vec - normal * 2.0 * (vec % normal)) * !*this;
}
end;

// Taken from Martin Beaudet's clVecteurs
function VectArcTan2( Y, X : Extended ) : Extended;
function VectArcCos( X : Single ) : Single;
function VectArcSin( X : Single ) : Single;
function DegreesToRadian( Degrees : Single ) : Single;

implementation

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

result := self;
end;

function TVector.Assign(aX, aY, aZ: TScalar): TVector;
begin
x := aX;
y := aY;
z := aZ;

result := self;
end;

constructor TVector.Create( aX : TScalar = 0; aY : TScalar = 0; aZ : TScalar = 0 );
begin
inherited Create;
x := aX;
y := aY;
z := aZ;
end;

constructor TVector.Create( const vec : TVector );
begin
inherited Create;
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;

begin
aAddedVector.Assign( x + vec.x, y + vec.y, z + vec.z )
end;

procedure TVector.Subtract( const vec : TVector; var aSubtractedVector : TVector );
begin
if aSubtractedVector <> nil then
aSubtractedVector.Assign( x - vec.x, y - vec.y, z - vec.z );
end;

procedure TVector.Negative( var aNegativeVector : TVector );
begin
if aNegativeVector <> nil then
aNegativeVector.Assign( -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;

procedure TVector.CrossProduct( const vec : TVector; var aCrossProductedVector : TVector );
begin
if aCrossProductedVector <> nil then
aCrossProductedVector.Assign( 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.LengthSquare: TScalar;
begin
result := ( x * x + y * y + z * z );
end;

function TVector.UnitVector : TVector;
var
Len : Single;
begin
// Store the Length
Len := Length;

x := x / Len;
y := y / Len;
z := z / Len;

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;

procedure TVector.Reflection( const normal : TVector; var aReflectionVector : TVector );
begin
if aReflectionVector <> nil then
begin
aReflectionVector.Assign( self );
aReflectionVector.Subtract( normal.Scale( 2.0 * ( aReflectionVector.DotProduct( normal ) ) ), aReflectionVector );
aReflectionVector.Scale( Length );
aReflectionVector.Normalize; // normalize this vector
end;
end;

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

result := self;
end;

procedure TVector.Positive( var aPositiveVector : TVector );
begin
if aPositiveVector <> nil then
aPositiveVector.Assign( +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;

function VectArcTan2( Y, X : Extended ) : Extended;
asm
FLD Y
FLD X
FPATAN
FWAIT
end;

function VectArcCos( X : Single ) : Single;
begin
Result := VectArcTan2( Sqrt( 1 - X * X ), X );
end;

function VectArcSin( X : Single ) : Single;
begin
result := VectArcTan2( X, Sqrt( 1 - X * X ) );
end;

begin
result := Radian * ( 180 / PI );
end;

function DegreesToRadian( Degrees : Single ) : Single;
begin
result := Degrees * ( PI / 180 );
end;

end.

Next on the list is a basic camera class.

Sly
06-07-2005, 10:14 PM
Yes, that looks cleaner.

UnitVector and Normalize do the same thing. No need to have two methods doing the same thing. Collapse it into one method.

function TVector.Normalize&#58; TVector;
var
Len&#58; TScalar;
begin
Result &#58;= Self;
Len &#58;= Length;
if Len > 0.0 then
Scale&#40;1.0 / Len&#41;;
end;
Some other things to mention there. You have TScalar declared, so use it consistently. You have used Single in some places throughout the code and TScalar everywhere else.

A quick optimization: When if..then statements are used, try to have the case that is most often true in the 'then' part. This improves branch prediction performance in the CPU.

I've almost finished my Vector unit. It would be interesting to compare performance between the two.

Clootie
07-07-2005, 05:36 AM
As TVector is a class then you don't need to use var for returning/changing instance. For example:
begin
aAddedVector.Assign( x + vec.x, y + vec.y, z + vec.z )
end;

will be better implemented as (will have one less memory dereference):

begin
aAddedVector.Assign( x + vec.x, y + vec.y, z + vec.z )
end;

PS. You probably know what "const vec : TVector" in ObjectPascal doesn't prevent from modifying vec internal data.

Sly
07-07-2005, 05:44 AM
True. I saw that, but didn't think much about it.

http://dennishomepage.gugs-cats.dk/CodingForSpeedInDelphi.doc (~1MB)

It is chock full of optimization tips. Small simple, some complex, and right down to getting a few CPU cycles shaved off a small routine.

Clootie
07-07-2005, 07:29 AM
TVector.SetLength should be better implemented as:
function TVector.SetLength( LengthLimit : TScalar ) : TVector;
begin
result := Scale( (LengthLimit*LengthLimit) / LengthSquare );
end;

Hmmm, something really wrong with:
procedure TVector.Reflection( const normal : TVector; var aReflectionVector : TVector );
begin
...
aReflectionVector.Scale( Length );
aReflectionVector.Normalize; // normalize this vector
...
end;

Sly
07-07-2005, 10:37 AM
TVector.SetLength should be better implemented as:
function TVector.SetLength( LengthLimit : TScalar ) : TVector;
begin
result := Scale( (LengthLimit*LengthLimit) / LengthSquare );
end;
Unfortunately, no. You don't get the same result. For example, let's say the vector is 3 units long and we want to make it 4 units.

Scale := Limit / Length := 4 / 3 := 1.3
Scale := (Limit * Limit) / LengthSquare := (4 * 4) / (3 * 3) := 16 / 9 := 1.7

Different scale values. The first one is correct.

Hmmm, something really wrong with:
procedure TVector.Reflection( const normal : TVector; var aReflectionVector : TVector );
begin
...
aReflectionVector.Scale( Length );
aReflectionVector.Normalize; // normalize this vector
...
end;
Yes. Scaling then normalizing means the scale is a wasted operation.

Sly
07-07-2005, 10:47 AM
For comparison, here is my Vector unit. It is not yet tested for accuracy. It compiles under both Delphi and FPC. Unfortunately, under FPC if you want operator overloads, you cannot use Delphi compatibility mode. Without Delphi compatibility mode, you do not have the implied Result variable in functions. Hence the old-school method of assigning to the function name. I used to do that all the time when I was first learning Pascal at uni (Mac Pascal on a Macintosh 128K), but since I've been using Delphi I've become so used to having the Result variable. You will note that there was one function (VectorCreate) where I had no choice but to use the Result variable in Delphi because it failed compilation using the function name.

unit Vector;

interface

uses
Scalar, Matrix;

type
PVector = ^TVector;
TVector = record
case Integer of
0:
(
x: TScalar;
y: TScalar;
z: TScalar;
);
1:
(
v: array [0..3] of TScalar;
);
end;

{\$IFDEF FPC}
operator + (const v1: TVector; const v2: TVector) Result: TVector;
operator - (const v1: TVector; const v2: TVector) Result: TVector;
operator * (const v1: TVector; const v2: TVector) Result: TVector;
operator * (const v1: TVector; s: TScalar) Result: TVector;
operator * (const v1: TVector; const m: TMatrix) Result: TVector;
operator / (const v1: TVector; s: TScalar) Result: TVector;
operator = (const v1: TVector; const v2: TVector) Result: Boolean;
{\$ENDIF FPC}

function VectorAdd(const v1: TVector; const v2: TVector): TVector;
function VectorSubtract(const v1: TVector; const v2: TVector): TVector;
function VectorMultiply(const v1: TVector; const v2: TVector): TVector; overload;
function VectorMultiply(const v1: TVector; s: TScalar): TVector; overload;
function VectorDivide(const v1: TVector; s: TScalar): TVector;
function VectorEqual(const v1: TVector; const v2: TVector): Boolean;
function VectorDot(const v1: TVector; const v2: TVector): TScalar;
function VectorCross(const v1: TVector; const v2: TVector): TVector;
function VectorLength(const v1: TVector): TScalar;
function VectorLengthSq(const v1: TVector): TScalar;
function VectorInterpolate(const v1: TVector; const v2: TVector; t: TScalar): TVector;
procedure VectorNormalize(var v1: TVector);
function VectorNormal(const v1: TVector): TVector;
function VectorNormalTri(const v1: TVector; const v2: TVector; const v3: TVector): TVector;
function VectorAngle(const v1: TVector; const v2: TVector): TScalar;

function VectorCreate(const v1: TVector): TVector; overload;
function VectorCreate(x, y, z: TScalar): TVector; overload;
procedure VectorToArray(const v1: TVector; a: PScalar);

const
NullVector: TVector = (x: 0.0; y: 0.0; z: 0.0);
FwdVector: TVector = (x: 0.0; y: 0.0; z: 1.0);
LeftVector: TVector = (x: 1.0; y: 0.0; z: 0.0);
UpVector: TVector = (x: 0.0; y: 1.0; z: 0.0);

implementation

{\$IFDEF FPC}
operator + (const v1: TVector; const v2: TVector) Result: TVector;
begin
Result.x := v1.x + v2.x;
Result.y := v1.y + v2.y;
Result.z := v1.z + v2.z;
end;

operator - (const v1: TVector; const v2: TVector) Result: TVector;
begin
Result.x := v1.x - v2.x;
Result.y := v1.y - v2.y;
Result.z := v1.z - v2.z;
end;

operator * (const v1: TVector; const v2: TVector) Result: TVector;
begin
Result.x := v1.x * v2.x;
Result.y := v1.y * v2.y;
Result.z := v1.z * v2.z;
end;

operator * (const v1: TVector; s: TScalar) Result: TVector;
begin
Result.x := v1.x * s;
Result.y := v1.y * s;
Result.z := v1.z * s;
end;

operator * (const v1: TVector; const m: TMatrix) Result: TVector;
begin
Result.x := v1.x * m.m[0, 0] + v1.y * m.m[1, 0] + v1.z * m.m[2, 0] + m.m[3, 0];
Result.y := v1.x * m.m[0, 1] + v1.y * m.m[1, 1] + v1.z * m.m[2, 1] + m.m[3, 1];
Result.z := v1.x * m.m[0, 2] + v1.y * m.m[1, 2] + v1.z * m.m[2, 2] + m.m[3, 2];
end;

operator / (const v1: TVector; s: TScalar) Result: TVector;
var
r: TScalar;
begin
r := 1.0 / s;
Result.x := v1.x * r;
Result.y := v1.y * r;
Result.z := v1.z * r;
end;

operator = (const v1: TVector; const v2: TVector) Result: Boolean;
begin
Result := (v1.x = v2.x) and (v1.y = v2.y) and (v1.z = v2.z);
end;
{\$ENDIF FPC}

function VectorAdd(const v1: TVector; const v2: TVector): TVector;
begin
end;

function VectorSubtract(const v1: TVector; const v2: TVector): TVector;
begin
VectorSubtract.x := v1.x - v2.x;
VectorSubtract.y := v1.y - v2.y;
VectorSubtract.z := v1.z - v2.z;
end;

function VectorMultiply(const v1: TVector; const v2: TVector): TVector;
begin
VectorMultiply.x := v1.x * v2.x;
VectorMultiply.y := v1.y * v2.y;
VectorMultiply.z := v1.z * v2.z;
end;

function VectorMultiply(const v1: TVector; s: TScalar): TVector;
begin
VectorMultiply.x := v1.x * s;
VectorMultiply.y := v1.y * s;
VectorMultiply.z := v1.z * s;
end;

function VectorDivide(const v1: TVector; s: TScalar): TVector;
var
r: TScalar;
begin
r := 1.0 / s;
VectorDivide.x := v1.x * r;
VectorDivide.y := v1.y * r;
VectorDivide.z := v1.z * r;
end;

function VectorEqual(const v1: TVector; const v2: TVector): Boolean;
begin
VectorEqual := (v1.x = v2.x) and (v1.y = v2.y) and (v1.z = v2.z);
end;

function VectorDot(const v1: TVector; const v2: TVector): TScalar;
begin
VectorDot := v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
end;

function VectorCross(const v1: TVector; const v2: TVector): TVector;
begin
VectorCross.x := v1.y * v2.z - v1.z * v2.y;
VectorCross.y := v1.z * v2.x - v1.x * v2.z;
VectorCross.z := v1.x * v2.y - v1.y * v2.x;
end;

function VectorLength(const v1: TVector): TScalar;
begin
VectorLength := Sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
end;

function VectorLengthSq(const v1: TVector): TScalar;
begin
VectorLengthSq := v1.x * v1.x + v1.y * v1.y + v1.z * v1.z;
end;

function VectorInterpolate(const v1: TVector; const v2: TVector; t: TScalar): TVector;
begin
VectorInterpolate.x := v1.x + (v2.x - v1.x) * t;
VectorInterpolate.y := v1.y + (v2.y - v1.y) * t;
VectorInterpolate.z := v1.z + (v2.z - v1.z) * t;
end;

procedure VectorNormalize(var v1: TVector);
var
s: TScalar;
begin
s := VectorLength(v1);
if s > 0.0 then
begin
v1.x := v1.x * s;
v1.y := v1.y * s;
v1.z := v1.z * s;
end;
end;

function VectorNormal(const v1: TVector): TVector;
var
s: TScalar;
begin
s := VectorLength(v1);
if s > 0.0 then
begin
VectorNormal.x := v1.x * s;
VectorNormal.y := v1.y * s;
VectorNormal.z := v1.z * s;
end
else
begin
VectorNormal := NullVector;
end;
end;

function VectorNormalTri(const v1: TVector; const v2: TVector; const v3: TVector): TVector;
begin
VectorNormalTri := VectorNormal(VectorCross(VectorSubtract(v2, v1), VectorSubtract(v3, v1)));
end;

function VectorAngle(const v1: TVector; const v2: TVector): TScalar;
begin
VectorAngle := VectorDot(VectorNormal(v1), VectorNormal(v2));
end;

function VectorProject(const v1: TVector; const v2: TVector): TVector;
var
t: TVector;
begin
t := VectorNormal(v2);
VectorProject := VectorMultiply(t, VectorDot(v1, t));
end;

function VectorReflect(const v1: TVector; const n: TVector): TVector;
begin
if VectorAngle(v1, n) < 0.0 then
VectorReflect := VectorAdd(v1, VectorMultiply(VectorProject(v1, n), -2.0))
else
VectorReflect := v1;
end;

function VectorCreate(const v1: TVector): TVector;
begin
VectorCreate := v1;
end;

function VectorCreate(x, y, z: TScalar): TVector;
begin
VectorCreate.x := x;
VectorCreate.y := y;
VectorCreate.z := z;
end;

function VectorCreate(a: PScalar): TVector;
begin
{\$IFDEF FPC}
Move(a^, VectorCreate, SizeOf(TVector));
{\$ELSE}
Move(a^, Result, SizeOf(TVector));
{\$ENDIF FPC}
end;

procedure VectorToArray(const v1: TVector; a: PScalar);
begin
Move(v1, a^, SizeOf(TVector));
end;

end.

I tried some timings using these functions. Note that they may be rough, but the more iterations reduces the effect of test setup overhead.

VectorCreate &#40;10 iterations&#41; = 7892 cycles &#40;789 cycles per iteration&#41;
VectorCreate &#40;100 iterations&#41; = 4040 cycles &#40;40 cycles per iteration&#41;
VectorCreate &#40;1000 iterations&#41; = 37200 cycles &#40;37 cycles per iteration&#41;
VectorCreate &#40;10000 iterations&#41; = 370192 cycles &#40;37 cycles per iteration&#41;
VectorLength &#40;10 iterations&#41; = 1100 cycles &#40;110 cycles per iteration&#41;
VectorLength &#40;100 iterations&#41; = 4940 cycles &#40;49 cycles per iteration&#41;
VectorLength &#40;1000 iterations&#41; = 45240 cycles &#40;45 cycles per iteration&#41;
VectorLength &#40;10000 iterations&#41; = 448244 cycles &#40;44 cycles per iteration&#41;
VectorNormalTri &#40;10 iterations&#41; = 3228 cycles &#40;322 cycles per iteration&#41;
VectorNormalTri &#40;100 iterations&#41; = 27268 cycles &#40;272 cycles per iteration&#41;
VectorNormalTri &#40;1000 iterations&#41; = 269816 cycles &#40;269 cycles per iteration&#41;
VectorNormalTri &#40;10000 iterations&#41; = 2695328 cycles &#40;269 cycles per iteration&#41;
I do not know how much effect Delphi's compiler optimizations may have had on these. I tried turning optimizations off, but that does not disable all optimizations.

savage
08-07-2005, 11:31 AM
I consciously kept the var keyword in there so that it is clear that the Object will be changed. Maybe I should explicitly define it as an out parameter, but I am unsure if FreePascal supports that keyword.

Thanks to Sly and co for all their input on the Vector class.

Following is the proposed camera class....

unit camera;

interface

uses
vector;

type
TCamera = class
private
CalcVector : TVector; // A sort of cached dummry vector used in Update methods.
protected
// these are used for moving and changing camera orientation
// through the MoveTo/LookTo methods
initPosition, finalPosition : TVector;
initLookAt, finalLookAt : TVector;

LookAtVelocity : TVector; // velocity for looking at objects
LookAtAcceleration : TVector; // acceleration for looking at objects

procedure UpdateLookAt;
procedure UpdateMoveTo;
public
CameraPosition : TVector; // position of camera
CameraVelocity : TVector; // velocity of camera
CameraAcceleration : TVector; // acceleration of camera
CameraLookAt : TVector; // lookat vector

// up, forward, right vectors
CameraUp : TVector;
CameraForward : TVector;
CameraRight : TVector;

// yaw and pitch angles
CameraYaw : TScalar;
CameraPitch : TScalar;

ScreenWidth, ScreenHeight : integer;
ScreenCenterX, ScreenCenterY : integer;

constructor Create( aLook : TVector ); overload;
constructor Create( aPosition : TVector; aLook : TVector ); overload;
destructor Destroy; override;

procedure LookAt( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure LookAt( aLook : TVector ); overload;
procedure MoveTo( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure MoveTo( aPosition : TVector ); overload;

// right rotation along y-axis (yaw)
procedure RotateYaw( aRadians : TScalar );
procedure RotatePitch( aRadians : TScalar );
procedure RotateRoll( aRadians : TScalar );
end;

implementation

{ TCamera }

constructor TCamera.Create;
begin
inherited;
CameraPosition := TVector.Create( 0.0, 0.0, 0.0 );
CameraLookAt := TVector.Create( 0.0, 0.0, 1.0 );

CameraForward := CameraLookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraRight := TVector.Create( 1.0, 0.0, 0.0 );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;

CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

constructor TCamera.Create( aPosition, aLook : TVector );
begin
inherited Create;
CameraPosition := aPosition;
CameraLookAt := aLook.UnitVector;

CameraForward := CameraLookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraRight := TVector.Create( 1.0, 0.0, 0.0 );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;

CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

constructor TCamera.Create( aLook : TVector );
begin
inherited Create;
CameraPosition := TVector.Create( 0.0, 0.0, 0.0 );
CameraLookAt := aLook.UnitVector;

CameraForward := CameralookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraForward.CrossProduct( CameraUp, CameraRight );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;

CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

destructor TCamera.Destroy;
begin
if CameraPosition <> nil then
CameraPosition.Free;
if CameraLookAt <> nil then
CameraLookAt.Free;

if CameraForward <> nil then
CameraForward.Free;
if CameraUp <> nil then
CameraUp.Free;
if CameraRight <> nil then
CameraRight.Free;

if CameraVelocity <> nil then
CameraVelocity.Free;
if CameraAcceleration <> nil then
CameraAcceleration.Free;

if CalcVector <> nil then
CalcVector.Free;
inherited;
end;

procedure TCamera.LookAt( aX, aY, aZ : TScalar );
begin
CameraLookAt.x := aX;
CameraLookAt.y := aY;
CameraLookAt.y := aZ;
end;

procedure TCamera.LookAt( aLook : TVector );
begin
CameraLookAt.Assign( aLook );
end;

procedure TCamera.MoveTo( aX, aY, aZ : TScalar );
begin
CameraPosition.x := aX;
CameraPosition.y := aY;
CameraPosition.z := aZ;
end;

procedure TCamera.MoveTo( aPosition : TVector );
begin
CameraPosition.Assign( aPosition );
end;

procedure TCamera.RotatePitch( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraUp.y := cosine * CameraUp.Length;
CameraUp.z := sine * CameraUp.Length;

CameraForward.y := -sine * CameraForward.Length;
CameraForward.z := cosine * CameraForward.Length;
{* x y z p
| 1 0 0 0 |
M = | 0 cos(A) -sin(A) 0 |
| 0 sin(A) cos(A) 0 |
| 0 0 0 1 |
*}
end;

procedure TCamera.RotateRoll( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraRight.x := cosine * CameraRight.Length;
CameraRight.y := sine * CameraRight.Length;

CameraUp.x := -sine * CameraForward.Length;
CameraUp.y := cosine * CameraForward.Length;
{*
| cos(A) -sin(A) 0 0 |
M = | sin(A) cos(A) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
*}
end;

procedure TCamera.RotateYaw( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraRight.x := cosine * CameraRight.Length;
CameraRight.z := sine * CameraRight.Length;

CameraForward.x := -sine * CameraForward.Length;
CameraForward.z := cosine * CameraForward.Length;

{* x y z p
| cos(A) 0 -sin(A) 0 |
M = | 0 1 0 0 |
| sin(A) 0 cos(A) 0 |
| 0 0 0 1 |
*}
end;

procedure TCamera.UpdateLookAt;
begin
CalcVector.Assign( finalLookAt.x - CameralookAt.x,
finalLookAt.y - CameralookAt.y,
finalLookAt.z - CameralookAt.z );

LookAtVelocity.Assign( CalcVector.Scale( 0.5 ) );
end;

procedure TCamera.UpdateMoveTo;
begin
CalcVector.Assign( finalPosition.x - Cameraposition.x,
finalPosition.y - Cameraposition.y,
finalPosition.z - Cameraposition.z );

CameraVelocity.Assign( CalcVector.Scale( 0.5 ) );
end;

end.

The Sine and Cosine operations should probably be a look-up table to speed things up, if you are so inclined :).

Sly
08-07-2005, 11:59 AM
I consciously kept the var keyword in there so that it is clear that the Object will be changed. Maybe I should explicitly define it as an out parameter, but I am unsure if FreePascal supports that keyword.
out is the same as var except the value going in is ignored. const, var and out are only mostly useful when dealing with record types. For objects they have no real benefit.

// these are used for moving and changing camera orientation
// through the MoveTo/LookTo methods
initPosition, finalPosition : TVector;
initLookAt, finalLookAt : TVector;
These vectors are never used.

LookAtVelocity : TVector; // velocity for looking at objects
LookAtAcceleration : TVector; // acceleration for looking at objects
LookAtVelocity is referenced, but never created. LookAtAcceleration is never used.

procedure UpdateLookAt;
procedure UpdateMoveTo;
How are these methods called if they are in the protected section?

procedure LookAt( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure LookAt( aLook : TVector ); overload;
procedure MoveTo( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure MoveTo( aPosition : TVector ); overload;
I would provide a LookAt method that functions like most other LookAt functions where you provide the eye, target and up vectors.

// right rotation along y-axis (yaw)
procedure RotateYaw( aRadians : TScalar );
procedure RotatePitch( aRadians : TScalar );
procedure RotateRoll( aRadians : TScalar );
end;
How about a Rotate(aAngle: TVector) where each element of the vector is the angle in one axis? That is quite common. Instead of (x, y, z), it is (pitch, yaw, roll).

destructor TCamera.Destroy;
begin
if CameraPosition <> nil then
CameraPosition.Free;
if CameraLookAt <> nil then
CameraLookAt.Free;

if CameraForward <> nil then
CameraForward.Free;
if CameraUp <> nil then
CameraUp.Free;
if CameraRight <> nil then
CameraRight.Free;

if CameraVelocity <> nil then
CameraVelocity.Free;
if CameraAcceleration <> nil then
CameraAcceleration.Free;

if CalcVector <> nil then
CalcVector.Free;
inherited;
end;
You can call Free on a nil object with no ill effect. No need to check for nil before calling Free.

The Sine and Cosine operations should probably be a look-up table to speed things up, if you are so inclined :).
The difference is not that great these days with these operations implemented in the FPU. If you do want it to be faster, call SinCos instead of calling Sin and Cos separately.

Paulius
08-07-2005, 12:15 PM
From my experience on modern computers sin/cos tables are only beneficial when they can be used constantly, like in texture generation, but when you want good precision it might not fit in the cash. When you?¢_Tre unlikely to get a cash hit fpu functions are faster.

savage
29-08-2005, 12:00 PM
Appologies for the delay, RL has been busy... After looking at a few camera class implementations, I decided to split it into 2 classes. I genric Camera class, and one more specific GameCamera class. As mentioned all feedback, constructive critisism welcome.

So this is what it looks like so far...

unit camera;

interface

uses
vector;

type
TCamera = class
private

protected

public
CameraPosition : TVector; // position of camera
CameraVelocity : TVector; // velocity of camera
CameraAcceleration : TVector; // acceleration of camera
CameraLookAt : TVector; // lookat vector

// up, forward, right vectors
CameraUp : TVector;
CameraForward : TVector;
CameraRight : TVector;

// yaw and pitch angles
CameraYaw : TScalar;
CameraPitch : TScalar;

ScreenWidth, ScreenHeight : integer;
ScreenCenterX, ScreenCenterY : integer;

constructor Create( aLook : TVector ); overload; virtual;
constructor Create( aPosition : TVector; aLook : TVector ); overload; virtual;
destructor Destroy; override;

procedure LookAt( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure LookAt( aLook : TVector ); overload;
procedure MoveTo( aX : TScalar; aY : TScalar; aZ : TScalar ); overload;
procedure MoveTo( aPosition : TVector ); overload;

// right rotation along y-axis (yaw)
procedure RotateYaw( aRadians : TScalar );
procedure RotatePitch( aRadians : TScalar );
procedure RotateRoll( aRadians : TScalar );
procedure RotatePYR( aPitch : TScalar; aYaw : TScalar; aRoll : TScalar );
end;

implementation

{ TCamera }

constructor TCamera.Create;
begin
inherited;
CameraPosition := TVector.Create( 0.0, 0.0, 0.0 );
CameraLookAt := TVector.Create( 0.0, 0.0, 1.0 );

CameraForward := CameraLookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraRight := TVector.Create( 1.0, 0.0, 0.0 );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;
end;

constructor TCamera.Create( aPosition, aLook : TVector );
begin
inherited Create;
CameraPosition := aPosition;
CameraLookAt := aLook.UnitVector;

CameraForward := CameraLookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraRight := TVector.Create( 1.0, 0.0, 0.0 );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;
end;

constructor TCamera.Create( aLook : TVector );
begin
inherited Create;
CameraPosition := TVector.Create( 0.0, 0.0, 0.0 );
CameraLookAt := aLook.UnitVector;

CameraForward := CameralookAt;
CameraUp := TVector.Create( 0.0, 1.0, 0.0 );
CameraForward.CrossProduct( CameraUp, CameraRight );

CameraVelocity := TVector.Create( 0.0, 0.0, 0.0 );
CameraAcceleration := TVector.Create( 0.0, 0.0, 0.0 );

CameraYaw := 0.0;
CameraPitch := 0.0;
end;

destructor TCamera.Destroy;
begin
if CameraPosition <> nil then
CameraPosition.Free;
if CameraLookAt <> nil then
CameraLookAt.Free;

if CameraForward <> nil then
CameraForward.Free;
if CameraUp <> nil then
CameraUp.Free;
if CameraRight <> nil then
CameraRight.Free;

if CameraVelocity <> nil then
CameraVelocity.Free;
if CameraAcceleration <> nil then
CameraAcceleration.Free;
inherited;
end;

procedure TCamera.LookAt( aX, aY, aZ : TScalar );
begin
CameraLookAt.x := aX;
CameraLookAt.y := aY;
CameraLookAt.y := aZ;
end;

procedure TCamera.LookAt( aLook : TVector );
begin
CameraLookAt.Assign( aLook );
end;

procedure TCamera.MoveTo( aX, aY, aZ : TScalar );
begin
CameraPosition.x := aX;
CameraPosition.y := aY;
CameraPosition.z := aZ;
end;

procedure TCamera.MoveTo( aPosition : TVector );
begin
CameraPosition.Assign( aPosition );
end;

procedure TCamera.RotatePitch( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraUp.y := cosine * CameraUp.Length;
CameraUp.z := sine * CameraUp.Length;

CameraForward.y := -sine * CameraForward.Length;
CameraForward.z := cosine * CameraForward.Length;
{* x y z p
| 1 0 0 0 |
M = | 0 cos(A) -sin(A) 0 |
| 0 sin(A) cos(A) 0 |
| 0 0 0 1 |
*}
end;

procedure TCamera.RotatePYR(aPitch, aYaw, aRoll: TScalar);
begin
RotatePitch( aPitch );
RotateYaw( aYaw );
RotateRoll( aRoll );
end;

procedure TCamera.RotateRoll( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraRight.x := cosine * CameraRight.Length;
CameraRight.y := sine * CameraRight.Length;

CameraUp.x := -sine * CameraForward.Length;
CameraUp.y := cosine * CameraForward.Length;
{*
| cos(A) -sin(A) 0 0 |
M = | sin(A) cos(A) 0 0 |
| 0 0 1 0 |
| 0 0 0 1 |
*}
end;

procedure TCamera.RotateYaw( aRadians : TScalar );
var
sine, cosine : TScalar;
begin

CameraRight.x := cosine * CameraRight.Length;
CameraRight.z := sine * CameraRight.Length;

CameraForward.x := -sine * CameraForward.Length;
CameraForward.z := cosine * CameraForward.Length;

{* x y z p
| cos(A) 0 -sin(A) 0 |
M = | 0 1 0 0 |
| sin(A) 0 cos(A) 0 |
| 0 0 0 1 |
*}
end;

end.

And a more game specific one looks like this....

unit gamecamera;

interface

uses
gl,
glu,
vector,
camera,
gameobject;

type
TGameCamera = class( TCamera )
private
CalcVector : TVector; // A sort of cached dummry vector used in Update methods.
protected
// these are used for moving and changing camera orientation
// through the MoveTo/LookTo methods
initPosition, finalPosition : TVector;
initLookAt, finalLookAt : TVector;

LookAtVelocity : TVector; // velocity for looking at objects
LookAtAcceleration : TVector; // acceleration for looking at objects

procedure UpdateLookAt;
procedure UpdateMoveTo;
public
constructor Create; override;
constructor Create( aLook : TVector ); override;
constructor Create( aPosition : TVector; aLook : TVector ); override;
destructor Destroy; override;

procedure LookAtGameObject( aGameobject : TGameObject );
procedure LookAtGameObjectNow( aGameobject : TGameObject );
procedure MoveToGameObject( aGameobject : TGameObject );
procedure MoveToGameObjectNow( aGameobject : TGameObject );

// do animation/collision/ai/physics calculations
procedure Update( aDeltaTime : TScalar );
end;

implementation

{ TGameCamera }

procedure TGameCamera.Update( aDeltaTime : TScalar );
var
cosYaw, sinYaw, sinPitch, cosPitch, speed, strafeSpeed : single;
begin
if ( ( CameraYaw >= 360.0 ) or ( CameraYaw <= -360.0 ) ) then
CameraYaw := 0.0;

// Set boundaries for pitch
if ( CameraPitch > 60.0 ) then
CameraPitch := 60.0;
if ( CameraPitch < -60.0 ) then
CameraPitch := -60.0;

cosYaw := cos( DegreesToRadian( CameraYaw ) );
sinYaw := sin( DegreesToRadian( CameraYaw ) );
sinPitch := sin( DegreesToRadian( CameraPitch ) );

cosPitch := cos( DegreesToRadian( CameraPitch ) );

if ( speed > 15.0 ) then
speed := 15.0;
if ( strafeSpeed > 15.0 ) then
strafeSpeed := 15.0;
if ( speed < -15.0 ) then
speed := -15.0;
if ( strafeSpeed < -15.0 ) then
strafeSpeed := -15.0;

if ( CameraVelocity.Length > 0.0 ) then
begin
CameraVelocity.Negative( CameraAcceleration );
CameraAcceleration.Scale( 1.5 );
end;

CameraPosition.x := CameraPosition.x + cos( DegreesToRadian( CameraYaw + 90.0 ) ) * strafeSpeed;
CameraPosition.z := CameraPosition.z + sin( DegreesToRadian( CameraYaw + 90.0 ) ) * strafeSpeed;
CameraPosition.x := CameraPosition.x + cosYaw * speed;
CameraPosition.z := CameraPosition.z + sinYaw * speed;

CameraLookAt.x := CameraPosition.x + ( cosYaw * cosPitch );
CameraLookAt.y := CameraPosition.y + sinPitch;
CameraLookAt.z := CameraPosition.z + ( sinYaw * cosPitch );

gluLookAt( CameraPosition.x, CameraPosition.y, CameraPosition.z,
CameraLookAt.x, CameraLookAt.y, CameraLookAt.z,
0.0, 1.0, 0.0 );
end;

constructor TGameCamera.Create;
begin
inherited;
CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

constructor TGameCamera.Create(aLook: TVector);
begin
inherited;
CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

constructor TGameCamera.Create(aPosition, aLook: TVector);
begin
inherited;
CalcVector := TVector.Create( 0.0, 0.0, 0.0 );
end;

destructor TGameCamera.Destroy;
begin
if CalcVector <> nil then
CalcVector.Free;
inherited;
end;

procedure TGameCamera.LookAtGameObject( aGameobject : TGameObject );
begin
initLookAt.Assign( CameraLookAt );
finalLookAt.Assign( aGameobject.Position );

CameraLookAt.Scale( 0.25 ).Negative( LookAtAcceleration );

UpdateLookAt;
end;

procedure TGameCamera.LookAtGameObjectNow( aGameobject : TGameObject );
begin
CameralookAt.Assign( aGameobject.Position );
end;

procedure TGameCamera.MoveToGameObject( aGameobject : TGameObject );
begin
initPosition.Assign( CameraPosition );
finalPosition.Assign( aGameobject.Position );

CameraPosition.Scale( 0.25 ).Negative( CameraAcceleration );

UpdateMoveTo;
end;

procedure TGameCamera.MoveToGameObjectNow( aGameobject : TGameObject );
begin
CameraPosition.Assign( aGameobject.Position );
end;

procedure TGameCamera.UpdateLookAt;
begin
CalcVector.Assign( finalLookAt.x - CameralookAt.x,
finalLookAt.y - CameralookAt.y,
finalLookAt.z - CameralookAt.z );

LookAtVelocity.Assign( CalcVector.Scale( 0.5 ) );
end;

procedure TGameCamera.UpdateMoveTo;
begin
CalcVector.Assign( finalPosition.x - Cameraposition.x,
finalPosition.y - Cameraposition.y,
finalPosition.z - Cameraposition.z );

CameraVelocity.Assign( CalcVector.Scale( 0.5 ) );
end;

end.

Once these have been picked apart I'll post the proposed gamenode and gameobject classes.

Sly
29-08-2005, 12:28 PM
That's not too bad. I'm still not a fan of TVector as a class from a memory and efficiency point of view, but you are at least using it consistently.

When calling Free on a class instance, you do not need to check that the class instance is not nil first. Free does that already.

System.pas
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;

Not much else to comment on really. The class design seems to be pretty good.

savage
29-08-2005, 12:44 PM
Thanks for the quick feedback Steve. I am sure in older versions of Delphi if you called Free without checking for nil, it would AV. Hence an old habit dieing hard. I will check Delphi 4 later today to see if System.pas in there checked for nil in it's Free method.

savage
29-08-2005, 12:56 PM
I couldn't wait, I just checked Delphi 4's Free method and it looks like this...

procedure TObject.Free;
asm
TEST EAX, EAX
JE @@exit
MOV ECX, [EAX]
MOV DL, 1
CALL dword ptr [ECX], vmtDestroy
@@exit:
end;

I am not familiar with assembler at all, so someone will need to tell me if the TEST EAX, EAX is the same as if Self <> nil.

Paulius
29-08-2005, 05:03 PM
yup, it does exactlly that