Skip to content

Rotating the triangle and using dynamic data

In the last example we learned how to render a static scene with a triangle. We will start with that and implement a rotation. Subsequently you will learn how to render dynamic data.

Rendering a rotated triangle and dynamic data

The transform class

For describing the rotation a matrix is used. For a general introduction on vectors, matrices and transforms I can recommend this tutorial. For Pascal3D you do not necessarily understand how matrices are composed, though it helps. In Pascal3D a transform can be described with the TP3DTransform class. It is a frontend for all the common transformations (Translation, rotation, scale) which are then assembled to a matrix. In 3D math rotations can be described by matrices, euler angles or by quaternions (There are even more methods).

TP3DTransform = class( TP3DStreamable )
  ...
  // Common translations -->
  property Position: TVec3; // Translation for moving the scene
  property Scale: TVec3; // Scaling the scene along the global axes
    // for rotation use
  property Rotation: TVec3; // Rotation with euler angles around the global axes (XYZ)
    // or
  property Quaternion: TQuat; // Rotation in quaternion style (useful for bones, not prone to gimbal lock)
    // the other is then updated automatically
  // <-- Common translations

  property Direction: TVec3;  // Where is the transformed object pointing (useful for cameras or lights) [readonly]

  property Matrix: TMat4;  // Matrix that contains all the above transforms
  property MatrixInv: TMat4;  // Inverse matrix to undo the transformation
  property RotationOrder: TRotationOrder; // Determines in which order the transforms are applied
end;

The above listing shows the important properties of a TP3DTransform object in pseudo code. You can edit any of these and the others are automatically updated if necessary. Use the Position property if you want to shift the scene, the Scale property to for applying a scale and one of the two rotation types (Rotation or Quaternion) to rotate the scene. You can also set the Matrix property if you prefer to.

The transformations are typically distinguished between World, View and Projection. In the former the scene is transformed, in the second the camera. The difference is that to get the same visual results a View transform has to be opposite or inverse of the world transform. A camera that moves to the right, for example, causes the scene to shift optically to the left. The Projection transform decides how the three-dimensional scene is displayed on the screen. This can be either orthogonal or perspective.

Finally there is the RotationOrder. This determines which transformation is applied first. First moving a scene and then rotating it results in a different effect than the other way round.

Transforming the triangle

In this tutorial we use a world transform to rotate the triangle itself. Extending the code from the previous tutorial we can pass our World: TP3DTransform object in the settings of our triangle. However we first have to create our World transform object.

TMyApplication = class ( TP3DApplication )
  private
    FWorld: TP3DTransform;
    ...

  public
    ...
    property World: TP3DTransform read FWorld;
end;

And in our Initialize method we have to create the object on initialization. We create a P3D property that is the owner of the World object. We don't need to free it ourselves then as this is done automatically when the program terminates.

procedure TMyApplication.Initialize;
var
  Prop: TP3DStreamableContainer; // Use a local variable for the property as we do not need to keep track of it
begin
  ...
  Prop:= TP3DStreamableContainer.Create( 'World', TP3DTransform ); // Creating a P3D property that owns our transform object
  Properties.Add( Prop );  // Add the property to the property list of the application. This way both the property and the object are freed automatically on termination
  FWorld:= TP3DTransform.Create( Prop );  // Create the object itself and specify it's owner: the property
end;

In the SetupTriangle method we then pass the the transform object in the settings of our geometry. Optionally we also change the coordiates to that of an equilateral triangle with a center at the origin which is also used as a center for the rotation.

procedure TMyApplication.SetupTriangle;
var
  h: Float;
begin
  h:= sqrt( 3 );
  FTriangleList:=
    layers([
      command_clear([ cfColor, cfDepth ], Grey500 ), // Clear the background and depth buffer

      geom_polygon([ vec3( -1,-1/3*h, 0 ), vec3( 1, -1/3*h, 0 ), vec3( 0, 2/3*h, 0 )], // will form an equilateral triangle with a center at the origin
        settings([
          uniform_world( World ),
          attrib( P3DAttribColor, [ Red500, Green500, Blue500 ])
        ])
      )
    );
end;

In the render procedure we do our rotation for each frame.

procedure TMyApplication.Render;
begin
  inherited Render;
  World.Rotation:= vec3( 0, 0, GetTickCount64 / 10 );
  World.Scale:= vec3( sin( GetTickCount64 / 1000 ) / 4 + 0.25 );
  // or by equivalently setting the matrix property
  // World.Matrix:= mat4rotate( vec3_Axis_PZ, GetTickCount64 / 1000 ) * mat4scale( vec4( vec3( sin( GetTickCount64 / 1000 ) / 4 + 0.25 ), 1 ));
  // The rotation is performed before the scale, which does however not matter in this case

  FTriangleList.Execute;
end;

Making the content adapt to a resize of the window

When we resize the window depending on the ratio of width and height the content might look stretched and thus our triangle is not equilateral on the screen anymore. To prevent this we are going to use a Projection transform that covers the ratio of width and height. By default an identity matrix is used that assumes that the ratio of width and height is 1:1. This is however normally not the case.

We add the Projection transform similar to the World transform by adding it to the application ...

TMyApplication = class ( TP3DApplication )
  private
    FProj: TP3DTransform;
    ...

  public
    ...
    property Proj: TP3DTransform read FProj;
end;

... and to the SetupTriangle and Initialize method.

procedure TMyApplication.SetupTriangle;
var
  h: Float;
begin
  h:= sqrt( 3 );
  FTriangleList:=
    layers([
      command_clear([ cfColor, cfDepth ], Grey500 ), // Clear the background and depth buffer

      geom_polygon([ vec3( -1,-1/3*h, 0 ), vec3( 1, -1/3*h, 0 ), vec3( 0, 2/3*h, 0 )],
        settings([
          uniform( 'proj', Proj ),
          uniform_world( World ),
          attrib( P3DAttribColor, [ Red500, Green500, Blue500 ])
        ])
      ),

      geom_lines( SineWave,
        settings([
          attrib( P3DAttribColor, SineWaveColors )
        ])
      )
    ]);
end;

procedure TMyApplication.Initialize;
var
  Prop: TP3DStreamableContainer;
begin
  ...
  Prop:= TP3DStreamableContainer.Create( 'Proj', TP3DTransform );
  Properties.Add( Prop );
  FProj:= TP3DTransform.Create( Prop );
end;

We then override the ResizeWindow method that is called whenever the window is resized.

TMyApplication = class ( TP3DApplication )
  public
    ...
    procedure ResizeWindow(Sender: TP3DWindow; Event: TSDL_WindowEvent); override;
end;
...

procedure TMyApplication.ResizeWindow( Sender: TP3DWindow; Event: TSDL_WindowEvent );
var
  Aspect: Extended;
begin
  inherited ResizeWindow( Sender, Event );
  Aspect:= P3DViewports.Screen.Width // P3DViewports.Screen.Height; Calculate the ratio of width and height
  Proj.Scale:= vec3( 1/Aspect, 1, 1 ); // and apply a scale to the visualization
end;

Handling dynamic data

Another advantage of a TP3DTransform object is that we don't have to update the render list in every frame although the visualization is different. P3DPlot is even capable of handling dynamic data that change their vertices or colors in every frame. To demonstrate that in the next step we are creating a scrolling sine wave with varying colors.

In the main class of our application we have to add two new buffer objects. That of the coordinates and that of the colors. For the triangle we passed our coordiates in an array. We could have used buffers there as well. If we choose to use an array P3DPlot will create the buffer object behind the scene. With dynamic data however we have are restricted to buffers. The buffer object is however similar to an array and works like a TList object. The objects can have several types depending on the underlying data (eg. TP3DVec3BufferGL for a list of three component vectors). The auto suffix that has to be used with P3DPlot determines that the object is managed by the engine and is freed when there are no references to it anymore. This happens if when the object holding the variable is freed and it is not referenced by the plot list anymore. We chose a three component vector for the positions and a four component vector for the colors (RGBA).

TMyApplication = class ( TP3DApplication )
  private
    FSineWave: TP3DVec3BufferGLAuto;
    FSineWaveColors: TP3DVec4BufferGLAuto;
    ...

  public
    ...
    property SineWave: TP3DVec3BufferGLAuto read FSineWave;
    property SineWaveColors: TP3DVec4BufferGLAuto read FSineWaveColors;
end;

As you can see the implementation is very straight forward. We add these lines to our SetupTriangle method.

procedure TMyApplication.SetupTriangle;
var
  h: Float;
begin
  h:= sqrt( 3 );
  FTriangleList:=
    layers([
      command_clear([ cfColor, cfDepth ], Grey500 ), // Clear the background and depth buffer

      geom_polygon([ vec3( -1,-1/3*h, 0 ), vec3( 1, -1/3*h, 0 ), vec3( 0, 2/3*h, 0 )],
        settings([
          uniform( 'proj', Proj ),
          uniform_world( World ),
          attrib( P3DAttribColor, [ Red500, Green500, Blue500 ])
        ])
      ),

      geom_lines( SineWave,
        settings([
          attrib( P3DAttribColor, SineWaveColors )
        ])
      )
    ]);
end;

In the render procedure we have to fill the data. We set the count of the Buffers to 250 points for that but you can play with the number. I will not go into very much detail about the formula for the points. The color is composed by an HSV color model where we use the scaled looping variable to determine the hue of the vertex.

Hint

When working with dynamic data don't forget to call the PushData method after modifying it to transfer it to the OpenGL object of the buffer.

procedure TMyApplication.Render;
const
  NumPoints = 250;
var
  i: Integer;
begin
  inherited Render;
  View.Rotation:= vec3( 0, 0, GetTickCount64 / 10 );
  View.Scale:= vec3( sin( GetTickCount64 / 1000 ) / 4 + 0.25 );

  FSineWave.I.Count:= NumPoints;
  FSineWaveColors.I.Count:= NumPoints;

  for i:= 0 to NumPoints - 1 do begin
    SineWave.I[ i ]:= vec3( i / NumPoints * 2 - 1, sin( i / NumPoints * 50 + GetTickCount64 / 100 ) / 20, 0.5 );
    SineWaveColors.I[ i ]:= vec4( P3DHSVToRGB( vec3( sin( i / NumPoints * 10 + GetTickCount64 / 100 ) / 2 + 0.5, sin( GetTickCount64 / 1000 ) / 4 + 0.75, 1 )), 1 - ( i / NumPoints ));
  end;

  FSineWave.I.PushData;
  FSineWaveColors.I.PushData;

  FTriangleList.Execute;
end;