Skip to content

3D Physics Engine

Open-Lotto Wiki edited this page Jun 5, 2026 · 1 revision

3D Physics Engine

The --gui 3D mode renders a real-time physically simulated lottery drum using OpenGL (fixed-function pipeline, no GLU or external math library).


Overview

When --gui 3D is passed, gui_opengl.c takes over and runs the full draw animation:

  1. Balls fall into a spinning drum.
  2. The drum rotates; balls collide with the shell and each other.
  3. One ball per pick is ejected from the drum and placed in the result row below.
  4. After all main numbers are drawn, the extra drum (if any) runs its own cycle.

Dual Drum Architecture

Games with extra numbers (Eurojackpot, Lotto 6aus49) show two drums side by side:

Drum Position (world X) Radius Ball range
Main drum −390 250 Main number range
Extra drum +360 150 Extra number range

The extra drum starts in a waiting state and only begins spinning after the main drum has finished all its picks.

DrumInstance struct

Each drum is a self-contained DrumInstance:

typedef struct {
    /* Balls */
    DrumBall *balls;
    int       ball_count;
    int       ball_min;       /* number label offset */

    /* Geometry */
    float drum_radius;
    float world_x, world_y;

    /* Animation state machine */
    DrumPhase phase;
    float     sim_time;
    float     drum_rotation_x, drum_rotation_y, drum_rotation_z;
    float     stop_omega;

    /* Draw cycle */
    int   picks_done, picks_total;
    float phase_timer;
    float spin_before_pick;
    int   current_pick_idx;
    int   waiting;            /* extra drum waits for main to finish */

    /* Results */
    int draw_numbers[16];
    int result_balls[16];
    int result_ball_count;

    /* Textures */
    GLuint *number_textures;
    int     texture_count;
} DrumInstance;

Ball Physics

Constants

Constant Value Purpose
DRUM_RADIUS 250.0 Main drum shell radius
BALL_RADIUS 18.0 Radius of each numbered ball
BALL_ROLLING_FRICTION 0.45 Rolling resistance coefficient
BALL_ANGULAR_DAMPING 0.985 Angular velocity decay per frame
BALL_SHELL_FRICTION 0.22 Friction between ball and drum shell

DrumBall struct

Each ball tracks both linear and angular state:

typedef struct {
    float x, y, z;          /* position */
    float vx, vy, vz;       /* linear velocity */
    float rot_x, rot_y, rot_z;   /* rotation angles (degrees) */
    float omega_x, omega_y, omega_z; /* angular velocity (deg/frame) */
    int   number;            /* displayed label */
    int   picked;            /* 1 = ejected from drum */
} DrumBall;

Collision pipeline (per frame)

  1. Gravity — apply GRAVITY downward acceleration to each ball.
  2. Shell collision — if |pos| > drum_radius - BALL_RADIUS, reflect velocity off the shell normal and apply BALL_SHELL_FRICTION tangential damping. Transfer spin via cross-product (tangent × normal).
  3. Ball–ball collision — O(n²) pairwise check. On penetration, apply impulse along the contact normal. Compute tangential impulse and update angular velocities of both balls.
  4. Rolling frictionapply_rolling_friction() damps angular velocity by BALL_ANGULAR_DAMPING per frame and converts spin to linear motion proportional to BALL_ROLLING_FRICTION.

Drum rotation

The drum shell itself rotates around its local X and Z axes. Ball positions are updated in world space; the shell rotation modulates the effective gravity direction experienced by balls inside, creating the tumbling effect.


State Machine

Each drum runs an independent state machine:

FALLING ──▶ ROTATING ──▶ STOPPING ──▶ PICK_PAUSE ──▶ (repeat for each pick)
                                                    └──▶ DRAW_COMPLETE
Phase Description
FALLING Balls spawn above the drum and fall in under gravity
ROTATING Drum spins at full speed; balls tumble for spin_before_pick seconds
STOPPING Drum decelerates smoothly
PICK_PAUSE One ball is ejected and placed in the result row; brief pause
DRAW_COMPLETE All picks done; drum idles

The extra drum's FALLING phase is blocked by waiting = 1 until the main drum reaches DRAW_COMPLETE.


Rendering

Ball textures

Ball number labels are rendered as SDL2 TTF surfaces, uploaded to OpenGL textures at startup by init_ball_textures(). Each ball number maps directly to a GLuint texture ID.

Per-frame render loop

For each drum:

  1. Draw the transparent drum shell (cylinder with alpha blending).
  2. For each non-picked ball: push matrix, translate to ball position, apply glRotatef using omega_x/y/z for tumbling animation, bind number texture, draw sphere quad.
  3. Draw the result row below the drum: picked balls rendered in a horizontal slot layout at Y = -(drum_radius + BALL_RADIUS * 3.5).

Camera

Single fixed camera at Z = −808, looking at origin. Both drums fit in the viewport at the default window size (1280 × 720).


GPU Compute (experimental)

The main drum optionally uses GPU-based physics via an OpenGL compute shader, enabled by setting the LOTTO_GPU_COMPUTE environment variable. The extra drum always uses CPU physics (≤ 12 balls, negligible cost).

LOTTO_GPU_COMPUTE=1 ./build/open-lotto --game "Eurojackpot" --gui 3D