Lesson: vectors5.nim

← Back to all lessons

Run Code Download Code

Source Code

# ****************************************************************************************
#
#   raylib [vectors] lesson 5 - The Transformation Trinity
#
#   This lesson demonstrates:
#   - The three fundamental transformations: Scale, Rotate, Translate.
#   - The matrix representation for each transformation.
#   - The difference between an object's description (Model Space) and the
#     underlying scene (World Space).
#   - How multiple objects can be transformed independently in the same space.
#
# ****************************************************************************************

import raylib
import raymath
import math

const
  screenWidth = 800
  screenHeight = 450

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

  # LESSON 1: OBJECT DESCRIPTION (MODEL SPACE)
  # An object is just a list of vertices (points) that describe its shape.
  # These are defined in "Model Space", meaning they are relative to the object's
  # own center (0,0), not the world.
  let triangleVertices = [
    Vector2(x: 0.0, y: -30.0),
    Vector2(x: -30.0, y: 30.0),
    Vector2(x: 30.0, y: 30.0)
  ]

  let quadVertices = [
    Vector2(x: -25.0, y: -25.0),
    Vector2(x: 25.0, y: -25.0),
    Vector2(x: 25.0, y: 25.0),
    Vector2(x: -25.0, y: 25.0)
  ]

  # LESSON 2: THE UNDERLYING SPACE (WORLD SPACE)
  # We can place multiple objects in our world. Each needs its own position.
  let triangleBasePos = Vector2(x: screenWidth * 0.25, y: screenHeight / 2.0)
  let quadWorldPos = Vector2(x: screenWidth * 0.75, y: screenHeight / 2.0)

  var time: float32 = 0.0

  # Main game loop
  # --------------------------------------------------------------------------------------
  while not windowShouldClose():
    # Update
    # ----------------------------------------------------------------------------------
    time += getFrameTime() # Accumulate time for smooth animation

    # LESSON 3: THE TRANSFORMATION MATRICES
    # We will create a unique transformation matrix for each object.

    # --- Triangle: Rotation and Translation ---
    let triRotation = time * 50.0 # Degrees

    # Create a diagonal movement from the base position towards the bottom-left.
    # The sin() function oscillates between -1.0 and 1.0. To use it for linear
    # interpolation (which needs a 0.0 to 1.0 factor), we must remap its range.
    # 1. Add 1.0 to shift the range from [-1, 1] to [0, 2].
    # 2. Divide by 2.0 to scale the range from [0, 2] to [0, 1].
    let movementFactor = (sin(time) + 1.0) / 2.0
    let startPos = triangleBasePos
    let endPos = Vector2(x: 100.0, y: screenHeight - 100.0)
    # "lerp" stands for Linear Interpolation. It finds a point on the line
    # between startPos and endPos. The movementFactor (0.0 to 1.0) determines
    # how far along that line the point is, creating smooth movement.
    let triangleWorldPos = lerp(startPos, endPos, movementFactor)

    # 1. The Rotation Matrix: Rotates the object around its own origin.
    # For 2D, this is a rotation around the Z-axis.
    # The `.degToRad` is a convenient converter provided by the `math` module.
    # `triRotation.degToRad` is equivalent to calling `degToRad(triRotation)`.
    let rotationMatrix: Matrix = rotateZ(triRotation.degToRad)

    # 2. The Translation: This is handled by vector addition later on (see LESSON 4).
    # --- Quad: Scaling and Translation ---
    # Create a "pulsing" scale effect using the sine function.
    const pulseFrequency = 2.0 # How fast the pulse is.
    const pulseAmplitude = 0.4 # How intense the pulse is (from 0.0 to 1.0).
    let scaleFactor = 1.0 + sin(time * pulseFrequency) * pulseAmplitude

    # 3. The Scaling Matrix: Stretches or shrinks the object from its origin.
    # It looks like this, with scale factors on the diagonal:
    # | sx  0   0   0 |
    # | 0   sy  0   0 |
    # | 0   0   sz  0 |
    # | 0   0   0   1 |
    # Even in 2D, raylib uses a 4x4 matrix, so we provide a Z-scale of 1.0
    # to signify "no change" on the unused Z-axis.
    # The 'W' component in the bottom-right is implicitly set to 1.0 by the `scale`
    # function itself, as this is required for a standard affine transformation matrix.
    # We only need to provide the arguments the function asks for (sx, sy, sz).
    let scalingMatrix: Matrix = scale(scaleFactor, scaleFactor, 1.0)

    # 4. The Translation Matrix: Moves the object to its final world position.
    let quadTranslationMatrix: Matrix = translate(quadWorldPos.x, quadWorldPos.y, 0.0)

    # Combine them into a single Model Matrix.
    # IMPORTANT: Matrix multiplication order is the reverse of the transformation order.
    # To achieve the effect of "1. Scale, then 2. Translate", we must multiply
    # the matrices as `M_translate * M_scale`.
    let quadModelMatrix: Matrix = multiply(quadTranslationMatrix, scalingMatrix)
    
    # LESSON 4: TRANSFORMING THE VERTICES (CPU-side)
    # For the triangle, we'll demonstrate a "hybrid" transformation approach.
    # This is different from the full matrix method in vectors3.nim.
    # Step 1: Rotate the vertex in its local space using the rotation matrix.
    # Step 2: Translate the now-rotated vertex to its world position by adding the position vector.
    # This method can be more intuitive for simple cases but is less scalable than
    # using a single, combined model matrix for all transformations.
    let transformedTriV1 = transform(triangleVertices[0], rotationMatrix) + triangleWorldPos
    let transformedTriV2 = transform(triangleVertices[1], rotationMatrix) + triangleWorldPos
    let transformedTriV3 = transform(triangleVertices[2], rotationMatrix) + triangleWorldPos

    # For clarity and scalability, let's put the transformed quad vertices into an array.
#[  let transformedQuadV1 = transform(quadVertices[0], quadModelMatrix)
    let transformedQuadV2 = transform(quadVertices[1], quadModelMatrix)
    let transformedQuadV3 = transform(quadVertices[2], quadModelMatrix)
    let transformedQuadV4 = transform(quadVertices[3], quadModelMatrix) ]#
    # For the quad, we use the full model matrix and loop through the vertices.
    var transformedQuadVertices: array[4, Vector2]
    for i, v in quadVertices:
      # This loop is the Nim equivalent of Python's `for i, v in enumerate(...)`.
      transformedQuadVertices[i] = transform(v, quadModelMatrix)

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

    drawText("Rotation + Translation", triangleBasePos.x.int32 - 100, 50, 20, DarkGray)
    drawTriangleLines(transformedTriV1, transformedTriV2, transformedTriV3, Maroon)

    drawText("Scaling + Translation", quadWorldPos.x.int32 - 100, 50, 20, DarkGray)
    
    # Draw the quad's outline by connecting its transformed vertices.
    for i in 0 ..< transformedQuadVertices.len:
      let startPoint = transformedQuadVertices[i]
      # The modulo operator (%) ensures that the last vertex (index 3) connects
      # back to the first vertex (index 0), closing the shape. (3 + 1) mod 4 = 0.
      let endPoint = transformedQuadVertices[(i + 1) mod 4]
      drawLine(startPoint, endPoint, 2.0, DarkBlue)

    drawText("Each object has its own vertices (Model Space) and its own " &
             "transformation matrix.", 10, screenHeight - 30, 17, Gray)

    endDrawing()
    # ------------------------------------------------------------------------------------
  # De-Initialization
  # --------------------------------------------------------------------------------------
  closeWindow()
  # --------------------------------------------------------------------------------------

main()

#[ Think of it like a "connect-the-dots" puzzle.

Your original shape (quadVertices) is the set of numbered dots on the page 
in their starting positions.
The transformation matrix (quadModelMatrix) is a set of instructions 
that tells you where to move each individual dot.
The drawing functions (drawLine in your case) are you, with a pencil, 
drawing straight lines between the new positions of the dots.
The matrix itself has no concept of a "line"; 
it only knows how to take an input point (x, y) and output a new point (x', y'). ]#