Lesson: vectors3.nim

← Back to all lessons

Run Code Download Code

Source Code

# ****************************************************************************************
#
#   raylib [vectors] lesson 3 - Transformation with Matrices
#
#   This lesson demonstrates:
#   - Creating rotation and translation matrices.
#   - Combining matrices into a single "model" matrix.
#   - Transforming vertices from Model Space to World Space using the matrix.
#   - The efficiency of using a single matrix for multiple transformations.
#
# ****************************************************************************************

import raylib
import raymath
import math

const
  screenWidth = 800
  screenHeight = 450

proc main =
  initWindow(screenWidth, screenHeight, "raylib [vectors] lesson 3 - Matrices")
  setTargetFPS(60)

  # MODEL SPACE: The vertices are the same as in Lesson 2.
  let v1 = Vector2(x:  0.0, y: -25.0) # The top point of the triangle
  let v2 = Vector2(x: -25.0, y:  25.0) # The bottom-left point
  let v3 = Vector2(x:  25.0, y:  25.0) # The bottom-right point

  # This is the world position for our shape's center.
  let worldPosition = Vector2(x: screenWidth / 2.0, y: screenHeight / 2.0)

  # A variable to hold the current rotation of our shape in degrees.
  var rotation: float32 = 0.0

  # Main game loop
  # --------------------------------------------------------------------------------------
  while not windowShouldClose(): # Detect window close button or ESC key
    # Update
    # ----------------------------------------------------------------------------------
    rotation += 1.0
    let angleInRadians = rotation.degToRad

    # LESSON 1: CREATE INDIVIDUAL TRANSFORMATION MATRICES

      #     Basis Vectors (Orientation & Scale)      |  Perspective
      # ---------------------------------------------+---------------
      # | m0 (Xx)   m4 (Yx)   m8 (Zx)   m12 (Tx) |   <- X-component of each axis
      # | m1 (Xy)   m5 (Yy)   m9 (Zy)   m13 (Ty) |   <- Y-component of each axis
      # | m2 (Xz)   m6 (Yz)   m10(Zz)   m14 (Tz) |   <- Z-component of each axis
      # +--------------------------------------------+
      # | m3        m7        m11       m15 (W)  |   <- Homogeneous Coordinate
      # ---------------------------------------------
      #   ^         ^         ^          ^
      #   |         |         |          |
      # X-axis    Y-axis    Z-axis    Translation (Position)


    # A matrix can store transformations. We create one for rotation and one for translation.

    # A 2D rotation matrix (embedded in raylib's 4x4 matrix) looks like this:
    # | cos(a)  -sin(a)   0   0 |
    # | sin(a)   cos(a)   0   0 |
    # |   0        0      1   0 |
    # |   0        0      0   1 |
    # where 'a' is the angle. `rotateZ` creates this for us.

    # The `rotateZ` function creates a matrix where the X and Y axis columns
    # are rotated around the Z axis. For an angle 'a', the first two columns become:
    # X-axis column: (cos(a), sin(a), 0)
    # Y-axis column: (-sin(a), cos(a), 0)

    let rotationMatrix = rotateZ(angleInRadians)
    
    # We create a matrix that will move our shape to its world position.
    # A translation matrix stores the translation values in the final column:
    # | 1  0  0  tx |
    # | 0  1  0  ty |
    # | 0  0  1  tz |
    # | 0  0  0  1  |
    # where (tx, ty, tz) is the amount to move. This is the m12, m13, m14 part of the matrix.

    # The `translate` function creates an "identity" matrix (which does nothing)
    # and then puts the translation values into the 4th column.
    # X-axis: (1, 0, 0)
    # Y-axis: (0, 1, 0)
    # Z-axis: (0, 0, 1)
    # Translation Column: (worldPosition.x, worldPosition.y, 0)

    let translationMatrix = translate(worldPosition.x, worldPosition.y, 0)

    # LESSON 2: COMBINE MATRICES
    # The power of matrices is that they can be multiplied together to combine their
    # transformations. The order is crucial and is the reverse of the operation order.
    # To achieve the effect of "1. Translate, then 2. Rotate", we must multiply
    # the matrices as `M_rotate * M_translate`.
    # The resulting `modelMatrix` now contains the rotated axes from the rotation
    # AND the position offset from the translation, all in one matrix.

    # We multiply in the order: `rotate * translate`
    let modelMatrix = multiply(rotationMatrix, translationMatrix)

    # LESSON 3: TRANSFORM VERTICES
    # Now, we apply our single, combined `modelMatrix` to each of the vertices.
    # The `transform` function performs the matrix-vector multiplication, which looks
    # like this: transformed_vector = original_vector * modelMatrix
    # This is much more efficient than doing a rotate and then an add for every vertex.

    let transformedV1 = transform(v1, modelMatrix)
    let transformedV2 = transform(v2, modelMatrix)
    let transformedV3 = transform(v3, modelMatrix)

    # To better visualize the transformation, we can define lines representing
    # the model's own local X and Y axes. The "model" is the triangle in its
    # original, untransformed state, centered at (0,0). This is its "local space".
    # These lines will be transformed by the exact same modelMatrix as the triangle
    # to show how the model's coordinate system is oriented in the world.
    const axisLength = 40.0
    let localXAxisStart = Vector2(x: -axisLength, y: 0) # A line from -40 to +40 on the local X-axis
    let localXAxisEnd   = Vector2(x: axisLength, y: 0)
    let localYAxisStart = Vector2(x: 0, y: -axisLength) # A line from -40 to +40 on the local Y-axis
    let localYAxisEnd   = Vector2(x: 0, y: axisLength)

    let worldXAxisStart = transform(localXAxisStart, modelMatrix)
    let worldXAxisEnd   = transform(localXAxisEnd, modelMatrix)
    let worldYAxisStart = transform(localYAxisStart, modelMatrix)
    let worldYAxisEnd   = transform(localYAxisEnd, modelMatrix)

    # Draw
    # ------------------------------------------------------------------------------------
    beginDrawing()
    clearBackground(RayWhite)

    drawText("This triangle is transformed by a single matrix!", 10, 10, 20, DarkGray)
    drawText("Gray lines are local axes. Blue lines are world axes.", 10, 40, 20, DarkGray)

    # Draw the static World Space axes (the blue lines) for reference.
    # These lines represent the fixed coordinate system of the screen/world.
    # They do not move or rotate.
    # The alpha value controls opacity. 1.0 is fully opaque, 0.0 is fully transparent.
    const axisAlpha = 0.4
    let worldXStart = Vector2(x: 0, y: worldPosition.y)
    let worldXEnd = Vector2(x: screenWidth, y: worldPosition.y)
    # 2.0 for line thickness
    drawLine(worldXStart, worldXEnd, 2.0, colorAlpha(Blue, axisAlpha))
    let worldYStart = Vector2(x: worldPosition.x, y: 0)
    let worldYEnd = Vector2(x: worldPosition.x, y: screenHeight)
    drawLine(worldYStart, worldYEnd, 2.0, colorAlpha(Blue, axisAlpha))
    
    # Draw the triangle's vertices after they have been transformed into world space.
    const centerMarkerRadius = 5.0
    drawTriangleLines(transformedV1, transformedV2, transformedV3, Maroon)
    # Draw a marker at the pivot point to make the center of the transformation visible.
    drawCircle(worldPosition, centerMarkerRadius, LightGray)

    # Draw the transformed local axes (the gray lines).
    # These represent the triangle's own coordinate system (its "Model Space").
    # By applying the same modelMatrix, we can see how the model's personal
    # X and Y axes are oriented in the world. Notice they rotate with the triangle.
    drawLine(worldXAxisStart, worldXAxisEnd, 2.0, LightGray)
    drawLine(worldYAxisStart, worldYAxisEnd, 2.0, LightGray)
    endDrawing()
    # ------------------------------------------------------------------------------------
  # De-Initialization
  # --------------------------------------------------------------------------------------
  closeWindow() # Close window and OpenGL context
  # --------------------------------------------------------------------------------------

main()