EAGL

EAGL Logo

Make it EAsier to work
with OpenGL in Elixir.

Overview

Most examples of working with OpenGL are written in C++ or C# (Unity). The purpose of the EAGL library is to:

The following are non-goals:

Quick Start

# Add to mix.exs
{:eagl, "~> 0.13.0"}

EAGL includes several examples to demonstrate its capabilities. Use the unified examples runner:

mix examples
════════════════════════════════════════════════════════════════
                         EAGL Examples Menu
════════════════════════════════════════════════════════════════

0. Non-Learn OpenGL Examples:
  01) Math Example - Comprehensive EAGL.Math functionality demo
  02) Teapot Example - 3D teapot with Phong shading

1. Learn OpenGL Getting Started Examples:

  Hello Window:     111) 1.1 Window    112) 1.2 Clear Colors

  Hello Triangle:   121) 2.1 Triangle  122) 2.2 Indexed    123) 2.3 Exercise1
                    124) 2.4 Exercise2 125) 2.5 Exercise3

  Shaders:          131) 3.1 Uniform   132) 3.2 Interpolation 133) 3.3 Class
                    134) 3.4 Exercise1 135) 3.5 Exercise2     136) 3.6 Exercise3

  Textures:         141) 4.1 Basic     142) 4.2 Combined      143) 4.3 Exercise1
                    144) 4.4 Exercise2 145) 4.5 Exercise3     146) 4.6 Exercise4

  Transformations:  151) 5.1 Basic     152) 5.2 Exercise1  153) 5.2 Exercise2

  Coordinate Systems: 161) 6.1 Basic   162) 6.2 Depth     163) 6.3 Multiple
                      164) 6.4 Exercise

  Camera:           171) 7.1 Circle    172) 7.2 Keyboard+DT 173) 7.3 Mouse+Zoom
                    174) 7.4 Camera Class 175) 7.5 Exercise1 (FPS) 176) 7.6 Exercise2 (Custom LookAt)

2. Learn OpenGL Lighting Examples:

  Colors:           211) 1.1 Colors
  Basic Lighting:   212) 2.1 Diffuse   213) 2.2 Specular

3. GLTF Examples:

  311) Box              312) Box Textured    313) Duck
  314) Box Animated     315) Damaged Helmet

════════════════════════════════════════════════════════════════
Enter code (01-02, 111-176, 211-218, 311-315), 'q' to quit, 'r' to refresh:
>

Usage

Math Operations

EAGL provides a comprehensive 3D math library based on GLM supporting:

Sigil Literals

EAGL provides three sigils for creating OpenGL data with compile-time validation and clean tabular formatting:

import EAGL.Math

# Matrix sigil (~m) - supports comments and automatic size detection
identity_4x4 = ~m"""
1.0  0.0  0.0  0.0
0.0  1.0  0.0  0.0
0.0  0.0  1.0  0.0
0.0  0.0  0.0  1.0
"""

transform_matrix = ~m"""
1.0  0.0  0.0  0.0
0.0  1.0  0.0  0.0
0.0  0.0  1.0  0.0
10.0 20.0 30.0 1.0  # Translation X, Y, Z (column-major: translation is in last column)
"""

# Vertex sigil (~v) - for raw vertex buffer data
triangle_vertices = ~v"""
# position      color
 0.0   0.5  0.0  1.0  0.0  0.0  # top vertex - red
-0.5  -0.5  0.0  0.0  1.0  0.0  # bottom left - green
 0.5  -0.5  0.0  0.0  0.0  1.0  # bottom right - blue
"""

# Index sigil (~i) - for element indices (must be integers)
quad_indices = ~i"""
0  1  3  # first triangle
1  2  3  # second triangle
"""

Vector and Matrix Operations

import EAGL.Math

# Vector operations
position = vec3(1.0, 2.0, 3.0)
direction = vec3(0.0, 1.0, 0.0) 
result = vec_add(position, direction)
length = vec_length(position)

# Matrix transformations
model = mat4_translate(vec3(5.0, 0.0, 0.0))
view = mat4_look_at(
  vec3(0.0, 0.0, 5.0),  # eye
  vec3(0.0, 0.0, 0.0),  # target
  vec3(0.0, 1.0, 0.0)   # up
)
projection = mat4_perspective(radians(45.0), 16.0/9.0, 0.1, 100.0)

Camera System

EAGL provides EAGL.Camera (geometric view/projection) and EAGL.OrbitCamera (orbit/zoom/pan controls built on it) for inspecting 3D models and scenes. glTF camera nodes load as EAGL.Camera via Node.get_camera/1:

use EAGL.OrbitCamera

# In setup - frame a scene (e.g. from glTF)
orbit = EAGL.OrbitCamera.fit_to_scene(scene)

# Or frame a bounding box
orbit = EAGL.OrbitCamera.fit_to_bounds({-1, -1, -1}, {1, 1, 1})

# In render
view = EAGL.OrbitCamera.get_view_matrix(orbit)
proj = EAGL.OrbitCamera.get_projection_matrix(orbit, w / h)
view_pos = EAGL.OrbitCamera.get_position(orbit)

Add use EAGL.OrbitCamera to inject default mouse/scroll event handlers. Override on_tick/2 for per-frame logic (e.g. animation updates) without losing those handlers—see example 04 (animated box) for the pattern. See the GLTF examples (01-05) for the full setup.

For LearnOpenGL tutorial examples (7.4-7.6, lighting chapter), a first-person WASD camera lives in examples/learnopengl/camera.ex as EAGL.Examples.LearnOpenGL.Camera.

Shader Management

The uniform helpers (from Wings3D) automatically detect the type of EAGL.Math values, eliminating the need to manually unpack vectors or handle different uniform types:

import EAGL.Shader

      # Compile and link shaders with type-safe shader types
      {:ok, vertex} = create_shader(:vertex, "vertex.glsl")
      {:ok, fragment} = create_shader(:fragment, "fragment.glsl")
      {:ok, program} = create_attach_link([vertex, fragment])

# Set uniforms with automatic type detection
set_uniform(program, "model_matrix", model_matrix)
set_uniform(program, "light_position", vec3(10.0, 10.0, 5.0))
set_uniform(program, "time", :erlang.monotonic_time(:millisecond))

# Or set multiple uniforms at once
set_uniforms(program, [
  model: model_matrix,
  view: view_matrix,
  projection: projection_matrix,
  light_position: vec3(10.0, 10.0, 5.0),
  light_color: vec3(1.0, 1.0, 1.0)
])

Texture Management

EAGL provides meaningful texture abstractions:

import EAGL.Texture
import EAGL.Error

# Load texture from image file (requires optional stb_image dependency)
{:ok, texture_id, width, height} = load_texture_from_file("priv/images/eagl_logo_black_on_white.jpg")

# Or create procedural textures for testing
{:ok, texture_id, width, height} = create_checkerboard_texture(256, 32)

# Manual texture creation and configuration
{:ok, texture_id} = create_texture()
:gl.bindTexture(@gl_texture_2d, texture_id)

      # Set texture parameters with type-safe keyword options
      set_texture_parameters(
        wrap_s: @gl_repeat,
        wrap_t: @gl_repeat,
        min_filter: @gl_linear_mipmap_linear,
        mag_filter: @gl_linear
      )

# Load pixel data with format handling
load_texture_data(width, height, pixel_data, 
  internal_format: :rgb,
  format: :rgb,
  type: :unsigned_byte
)

# Generate mipmaps and check for errors
:gl.generateMipmap(@gl_texture_2d)
check("After generating mipmaps")

# Use multiple textures
:gl.activeTexture(@gl_texture0)
:gl.bindTexture(@gl_texture_2d, texture1_id)
:gl.activeTexture(@gl_texture1)
:gl.bindTexture(@gl_texture_2d, texture2_id)

# Clean up
:gl.deleteTextures([texture_id])

Model Loading

OBJ Format

import EAGL.Model

# Load OBJ file (with automatic normal generation if missing)
{:ok, model} = load_model_to_vao("teapot.obj")

# Render the model
:gl.bindVertexArray(model.vao)
:gl.drawElements(@gl_triangles, model.vertex_count, @gl_unsigned_int, 0)

glTF 2.0 / GLB Format

EAGL supports loading glTF 2.0 models via the GLTF.EAGL bridge module, which converts glTF data structures into EAGL scene graphs with proper VAO/VBO creation.

# Load GLB, create scene graph, and attach shader program in one call
{:ok, scene, gltf, data_store} = GLTF.EAGL.load_scene("model.glb", shader_program)

# Load textures from the first material
{:ok, textures} = GLTF.EAGL.load_textures(gltf, data_store)

# Render using EAGL.Scene (handles transform hierarchy and uniforms)
EAGL.Scene.render(scene, view_matrix, projection_matrix)

See examples/gltf/ for progressive examples from a simple box to a PBR-textured helmet.

GLTF 2.0 Library

EAGL includes a comprehensive GLTF 2.0 library for representing complex 3D models and scenes. The library provides complete support for all GLTF 2.0 properties and follows the official specification.

GLTF Features

Basic Usage

# Create a basic GLTF document
gltf = GLTF.new("2.0", generator: "EAGL", copyright: "2024")

# Create a perspective camera
camera = GLTF.Camera.perspective(
  :math.pi() / 4,  # 45 degree field of view
  0.1,             # near plane
  aspect_ratio: 16.0 / 9.0,
  zfar: 100.0
)

# Create a scene with nodes
scene = GLTF.Scene.with_nodes([0, 1], name: "Main Scene")

# Create a material with PBR properties
pbr = GLTF.Material.PbrMetallicRoughness.new(
  base_color_factor: [0.8, 0.2, 0.2, 1.0],  # Red material
  metallic_factor: 0.0,
  roughness_factor: 0.5
)
material = GLTF.Material.new(pbr_metallic_roughness: pbr)

# Create nodes with transformations
camera_node = GLTF.Node.with_trs(
  [0.0, 2.0, 5.0],           # translation
  [0.0, 0.0, 0.0, 1.0],      # rotation (quaternion)
  [1.0, 1.0, 1.0],           # scale
  camera: 0
)

mesh_node = GLTF.Node.new(
  mesh: 0,
  material: 0
)

# Assemble the complete document
gltf = %{gltf |
  cameras: [camera],
  materials: [material],
  nodes: [camera_node, mesh_node],
  scenes: [scene],
  scene: 0
}

# Validate the document
case GLTF.validate(gltf) do
  :ok -> IO.puts("Valid GLTF document!")
  {:error, reason} -> IO.puts("Validation error: #{inspect(reason)}")
end

GLTF Properties Reference

The library implements all GLTF 2.0 properties as Elixir modules:

Core Document Structure:GLTF, GLTF.Asset, GLTF.Extension, GLTF.Extras

Scene and Hierarchy:GLTF.Scene, GLTF.Node, GLTF.Camera, GLTF.Camera.Perspective, GLTF.Camera.Orthographic

Geometry and Meshes:GLTF.Mesh, GLTF.Mesh.Primitive, GLTF.Accessor, GLTF.Accessor.Sparse, GLTF.Buffer, GLTF.BufferView

Materials and Textures:GLTF.Material, GLTF.Material.PbrMetallicRoughness, GLTF.Material.NormalTextureInfo, GLTF.Material.OcclusionTextureInfo, GLTF.Texture, GLTF.TextureInfo, GLTF.Image, GLTF.Sampler

Animation and Skinning:GLTF.Animation, GLTF.Animation.Channel, GLTF.Animation.Sampler, GLTF.Skin

Design Principles

GLTF Roadmap

GLB Loading HTTP Client Issue

On some macOS systems, Erlang's built-in :httpc HTTP client has a bug where http_util.timestamp/0 fails during HTTPS requests, causing GLB web loading to fail. Add the :req dependency and use http_client: :req when loading from URLs. See Troubleshooting: GLB Loading HTTP Client for details.

Buffer Management

EAGL provides type-safe, buffer management with automatic stride/offset calculation and standard attribute helpers.

import EAGL.Buffer

# Simple position-only VAO/VBO (most common case)
vertices = ~v"""
-0.5  -0.5  0.0
 0.5  -0.5  0.0
 0.0   0.5  0.0
"""
{vao, vbo} = create_position_array(vertices)

# Multiple attribute configuration - choose your approach:
# Position + color vertices (6 floats per vertex: x,y,z,r,g,b)
position_color_vertices = ~v"""
# position      color
-0.5  -0.5  0.0  1.0  0.0  0.0  # vertex 1: position + red
 0.5  -0.5  0.0  0.0  1.0  0.0  # vertex 2: position + green  
 0.0   0.5  0.0  0.0  0.0  1.0  # vertex 3: position + blue
"""

# APPROACH 1: Automatic calculation (recommended for standard layouts)
# Automatically calculates stride/offset - no manual math required.
attributes = vertex_attributes(:position, :color)
{vao, vbo} = create_vertex_array(position_color_vertices, attributes)

# APPROACH 2: Manual configuration (for fine control or non-standard layouts)  
# Specify exactly what you want - useful for custom stride, non-sequential locations, etc.
attributes = [
  position_attribute(stride: 24, offset: 0),      # uses default location 0
  color_attribute(stride: 24, offset: 12)         # uses default location 1
]
{vao, vbo} = create_vertex_array(position_color_vertices, attributes)

# APPROACH 3: Custom locations (override defaults)
attributes = [
  position_attribute(location: 5, stride: 24, offset: 0),    # custom location 5
  color_attribute(location: 2, stride: 24, offset: 12)       # custom location 2
]
{vao, vbo} = create_vertex_array(position_color_vertices, attributes)

# Use automatic approach when:  - Standard position/color/texture/normal layouts
#                               - Sequential attribute locations (0, 1, 2, 3...)
#                               - Tightly packed (no padding between attributes)
#
# Use manual approach when:     - Custom attribute locations or sizes
#                               - Non-standard data types or normalization 
#                               - Attribute padding or unusual stride patterns
#                               - Need to match specific shader attribute locations

# Indexed geometry (rectangles, quads, models)
quad_vertices = ~v"""
 0.5   0.5  0.0  # top right
 0.5  -0.5  0.0  # bottom right
-0.5  -0.5  0.0  # bottom left
-0.5   0.5  0.0  # top left
"""
indices = ~i"""
0  1  3  # first triangle
1  2  3  # second triangle
"""
{vao, vbo, ebo} = create_indexed_position_array(quad_vertices, indices)

# Complex interleaved vertex data with multiple attributes
# Format: position(3) + color(3) + texture_coord(2) = 8 floats per vertex
interleaved_vertices = ~v"""
# x     y     z     r     g     b     s     t
-0.5  -0.5   0.0   1.0   0.0   0.0   0.0   0.0  # bottom left
 0.5  -0.5   0.0   0.0   1.0   0.0   1.0   0.0  # bottom right
 0.0   0.5   0.0   0.0   0.0   1.0   0.5   1.0  # top centre
"""

# Three standard attributes with automatic calculation
{vao, vbo} = create_vertex_array(interleaved_vertices, vertex_attributes(:position, :color, :texture_coordinate))

# Clean up resources
delete_vertex_array(vao, vbo)
delete_indexed_array(vao, vbo, ebo)  # For indexed arrays

Standard Attribute Helpers:

Two Configuration Approaches:

  1. Automatic Layout (recommended): vertex_attributes() assigns sequential locations (0, 1, 2, 3...) and calculates stride/offset automatically
  2. Manual Layout: Individual attribute helpers allow custom locations, stride, and offset for non-standard layouts

Key Benefits:

Error Handling

import EAGL.Error

# Check for OpenGL errors with context
check("After buffer creation")  # Returns :ok or {:error, message}

# Get human-readable error string for error code
error_string(1280)  # "GL_INVALID_ENUM"

# Check and raise on error (useful for debugging)
check!("Critical operation")  # Raises RuntimeError if error found

Window Creation

EAGL provides flexible window creation with a clean, options-based API:

defmodule MyApp do
  use EAGL.Window
  import EAGL.Shader
  import EAGL.Math

  def run_example do
    # For 2D rendering (triangles, sprites, UI) - uses default 1024x768 size
    EAGL.Window.run(__MODULE__, "My 2D OpenGL App")
    
    # For 3D rendering (models, scenes with depth)
    EAGL.Window.run(__MODULE__, "My 3D OpenGL App", depth_testing: true)
    
    # For tutorials/examples with automatic ENTER key handling
    EAGL.Window.run(__MODULE__, "Tutorial Example", enter_to_exit: true)
    
    # Custom window size and options
    EAGL.Window.run(__MODULE__, "Custom Size App", size: {1280, 720}, depth_testing: true, enter_to_exit: true)
  end

  @impl true
  def setup do
    # Initialize shaders, load models, etc.
    {:ok, initial_state}
  end

  @impl true
  def render(width, height, state) do
    # Your render function should handle clearing the screen
    :gl.clearColor(0.2, 0.3, 0.3, 1.0)
    
    # For 2D rendering (depth_testing: false, default)
    :gl.clear(@gl_color_buffer_bit)
    
    # For 3D rendering (depth_testing: true)
    # :gl.clear(@gl_color_buffer_bit ||| @gl_depth_buffer_bit)
    
    # Render your content here
    :ok
  end

  @impl true
  def cleanup(state) do
    # Clean up resources
    :ok
  end

  # Optional: Handle input and animation events
  @impl true
  def handle_event(event, state) do
    case event do
      # Keyboard input (W/A/S/D for camera movement, ESC to exit, etc.)
      {:key, key_code} ->
        # Handle keyboard input - see camera examples for WASD movement
        {:ok, state}
      
      # Mouse movement (for first-person camera look around)
      {:mouse_motion, x, y} ->
        # Handle mouse look - see camera examples for implementation
        {:ok, state}
      
      # Scroll wheel (for camera zoom)
      {:mouse_wheel, _x, _y, _wheel_rotation, wheel_delta} ->
        # Handle scroll zoom - positive/negative wheel_delta for zoom in/out
        {:ok, state}
      
      # 60 FPS tick for animations and updates
      :tick ->
        # Update animations, physics, camera movement, etc.
        {:ok, updated_state}
      
      _ ->
        {:ok, state}
    end
  end
end

Requirements

Platform-specific Notes

All Platforms

EAGL uses Erlang's built-in wx module for windowing, which is included with standard Erlang/OTP installations. No additional GUI libraries need to be installed.

Linux

Ensure you have OpenGL drivers installed:

# Ubuntu/Debian
sudo apt-get install libgl1-mesa-dev libglu1-mesa-dev

# Fedora/RHEL
sudo dnf install mesa-libGL-devel mesa-libGLU-devel
WSL2 (Windows Subsystem for Linux)

EAGL runs on WSL2, but OpenGL performance is limited. Rendering goes through a software layer (WSLg or similar) rather than direct GPU access, which can cause:

For the smoothest experience, use native Linux, macOS, or Windows. WSL2 is fine for development and testing, but performance-sensitive applications may feel noticeably slower.

macOS

OpenGL is included with macOS. No additional setup required.

Important: EAGL automatically detects macOS and enables forward compatibility for OpenGL 3.0+ contexts, which is required by Apple's OpenGL implementation. This matches the behaviour of the #ifdef __APPLE__ code commonly found in OpenGL tutorials.

Version Sensitivity for OpenGL NIFs

macOS requires exact version matching between Erlang/OTP and Elixir for OpenGL Native Implemented Functions (NIFs) to load properly. Version mismatches will cause {:nif_not_loaded, :module, :gl, :line, N} errors when examples try to run.

Symptoms of version mismatch:

Solution: Use matching Erlang/OTP and Elixir versions. Check your current versions:

# Check current versions
elixir --version
# Should show matching OTP versions, e.g.:
# Erlang/OTP 26 [erts-14.2.1]
# Elixir 1.15.7 (compiled with Erlang/OTP 26)

If versions don't match (e.g., "OTP 28" with "compiled with Erlang/OTP 25"):

# List available versions
asdf list erlang
asdf list elixir

# Switch to matching versions (example)
asdf global erlang 26.2.1
asdf global elixir 1.15.7-otp-26

# Or update your project's .tool-versions file
echo "erlang 26.2.1" > .tool-versions
echo "elixir 1.15.7-otp-26" >> .tool-versions

Recommended version combinations:

Retina Display Support

EAGL automatically handles retina display scaling on macOS. The viewport will correctly fill the entire window regardless of display pixel density.

How it works:

What this means:

If you're using EAGL, retina support is automatic. If you're calling :gl.viewport() directly, use the dimensions passed to your render/3 function rather than calling :wxWindow.getSize() yourself.

Windows

OpenGL is typically available through graphics drivers. If you encounter issues, ensure your graphics drivers are up to date.

Installation

  1. Clone the repository:
    git clone https://github.com/yourusername/eagl.git
    cd eagl
  2. Install dependencies:
    mix deps.get
  3. Compile the project:
    mix compile
  4. Run tests to verify everything works:
    mix test
  5. Try the examples:
    mix examples

Project Structure

lib/
├── eagl/                   # Core EAGL modules
│   ├── animation.ex        # Keyframe animation system
│   ├── animation/          # Animation sub-modules (sampler, channel, timeline)
│   ├── animator.ex         # Animation playback controller (GenServer)
│   ├── buffer.ex           # VAO/VBO helper functions
│   ├── camera.ex           # First-person camera (LearnOpenGL style)
│   ├── const.ex            # OpenGL constants
│   ├── error.ex            # Error checking and reporting
│   ├── math.ex             # GLM-style math library
│   ├── camera.ex           # Geometric camera (view/projection)
│   ├── model.ex            # 3D model management
│   ├── node.ex             # Scene graph node with TRS transforms
│   ├── obj_loader.ex       # Wavefront OBJ parser
│   ├── orbit_camera.ex     # Orbit/zoom/pan camera with use macro
│   ├── scene.ex            # Scene graph with hierarchical rendering
│   ├── shader.ex           # Shader compilation
│   ├── texture.ex          # Texture loading and management
│   ├── window.ex           # Window management with adaptive frame timing
│   └── window_behaviour.ex # Window callback behaviour
├── gltf/                   # GLTF 2.0 library
│   ├── eagl.ex             # Bridge: GLTF to EAGL (loaders, shaders, uniforms)
│   ├── glb_loader.ex       # GLB file parser with HTTP caching
│   ├── accessor.ex         # Typed views into buffers
│   ├── animation.ex        # GLTF animation channels and samplers
│   ├── asset.ex            # Asset metadata
│   ├── binary.ex           # GLB binary structure
│   ├── buffer.ex           # Binary data containers
│   ├── buffer_view.ex      # Buffer subsets with stride
│   ├── camera.ex           # Perspective/orthographic cameras
│   ├── data_store.ex       # Binary data management
│   ├── gltf.ex             # Root document structure and loading
│   ├── image.ex            # Image data (external/embedded)
│   ├── material.ex         # PBR materials
│   ├── mesh.ex             # Mesh primitives
│   ├── node.ex             # Scene graph nodes
│   ├── sampler.ex          # Texture sampling parameters
│   ├── scene.ex            # Root nodes collection
│   ├── skin.ex             # Vertex skinning
│   ├── texture.ex          # Texture source and sampler
│   └── texture_info.ex     # Texture references in materials
examples/
├── math_example.ex         # Math library demonstrations
├── teapot_example.ex       # 3D teapot rendering
├── gltf/                   # Progressive GLTF examples
│   ├── 01_box.ex           # Simple indexed geometry
│   ├── 02_box_textured.ex  # Textures and materials
│   ├── 03_duck.ex          # Multi-node scene graph
│   ├── 04_box_animated.ex  # GLTF animation playback
│   └── 05_damaged_helmet.ex # Full PBR rendering
└── learnopengl/            # LearnOpenGL tutorial ports
    ├── camera.ex           # First-person camera (examples 7.4-7.6, lighting)
    ├── 1_getting_started/  # Chapters 1-7
    └── 2_lighting/         # Lighting chapter
test/
├── eagl/                   # Unit tests for EAGL modules
│   ├── animation_test.exs  # Animation system tests
│   ├── buffer_test.exs     # Buffer management tests
│   ├── error_test.exs      # Error handling tests
│   ├── math_test.exs       # Math library tests
│   ├── model_test.exs      # Model loading tests
│   ├── obj_loader_test.exs # OBJ parser tests
│   ├── camera_test.exs     # EAGL.Camera tests
│   ├── orbit_camera_test.exs # Orbit camera tests
│   ├── shader_test.exs     # Shader compilation tests
│   └── texture_test.exs    # Texture management tests
├── examples/
│   └── learnopengl/
│       └── camera_test.exs  # LearnOpenGL Camera tests
├── gltf/                   # GLTF module tests
│   ├── accessor_test.exs   # Accessor parsing tests
│   ├── asset_test.exs      # Asset metadata tests
│   ├── binary_test.exs     # GLB binary structure tests
│   ├── data_store_test.exs # Data store tests
│   ├── eagl_bridge_test.exs # GLTF-EAGL bridge tests
│   ├── glb_loader_test.exs # GLB loader tests
│   ├── mesh_test.exs       # Mesh primitive tests
│   └── scene_test.exs      # Scene tests
├── examples_test.exs       # Automated example tests
└── gltf_integration_test.exs # GLB integration tests
priv/
├── models/                 # 3D model files (.obj)
├── scripts/                # Convenience scripts
│   └── examples.exs        # Unified examples runner (mix examples)
└── shaders/                # GLSL shader files
    ├── gltf/               # Standard GLTF shaders (Phong, PBR)
    └── learnopengl/        # LearnOpenGL tutorial shaders

Features

Roadmap

The current focus is to:

And in future:

Troubleshooting

Common Issues

Example Testing Timeouts

Examples use automatic timeouts for testing and will exit cleanly after the specified duration:

# Run all tests including automated example tests
mix test

# Run only unit tests if you want to skip example testing
mix test test/eagl/

# Run automated example tests specifically
mix test test/examples_test.exs

IEx Break Prompt

If you encounter an unexpected error in IEx and see a BREAK: (a)bort prompt, this indicates a crash in the BEAM VM. Enter 'a' to abort and return to the shell, then investigate the error that caused the crash.

Test Timeouts in CI

Examples now use automatic timeouts and run successfully in continuous integration environments:

Platform-Specific Issues

OpenGL Context Creation Failures

If you encounter context creation errors:

Missing Dependencies

If optional dependencies are missing, EAGL will show warnings but continue with fallback behaviour:

GLB Loading HTTP Client Issue

On some macOS systems, Erlang's built-in :httpc HTTP client has a bug where http_util.timestamp/0 fails during HTTPS requests, causing GLB web loading to fail with errors like:

"function :http_util.timestamp/0 is undefined (module :http_util is not available)"

Solution: Add the :req HTTP client as a dependency and configure GLB loading to use it:

# In mix.exs
defp deps do
  [
    {:req, "~> 0.4"}  # Add this for reliable HTTP on macOS
    # ... other deps
  ]
end

# When loading GLB files from URLs
{:ok, glb} = GLTF.GLBLoader.parse_url(url, http_client: :req)

Symptoms: GLB web demos fail with "http_util.timestamp/0 is undefined"; local GLB files work fine; only URL loading fails.

GLTF Examples: Missing GLB Files

The GLTF examples (Box Textured, Duck, Box Animated, Damaged Helmet) require sample GLB files that are not in the repo. Download them first:

mix glb.samples

This fetches Box, BoxTextured, Duck, BoxAnimated, and DamagedHelmet from Khronos glTF-Sample-Assets into test/fixtures/samples/. The integration tests (mix test --include integration) also download these on demand.

GLTF Examples Exit Immediately on macOS

If GLTF examples close immediately on macOS while the basic Box example works:

  1. Check for setup errors: The examples runner reports setup/load failures. If you see "Example failed (setup or load error): ...", the issue is likely a missing GLB file (run mix glb.samples) or texture/shader loading.

  2. macOS wxGLCanvas timing: EAGL uses longer initialization delays on macOS. If the window still closes immediately, try clicking the window as soon as it appears to ensure it receives focus.

Contributing

We welcome contributions. Suggested contributions include:

Please read through these guidelines before submitting changes.

Development Setup

  1. Fork and clone the repository
  2. Install dependencies: mix deps.get
  3. Run tests to ensure everything works: mix test
  4. Download GLB samples for GLTF examples: mix glb.samples
  5. Try the examples: mix examples

Code Standards

Style Guidelines

Testing Requirements

Documentation Standards

Design Philosophy

EAGL focuses on meaningful abstractions rather than thin wrappers around OpenGL calls:

Provide Value

Avoid Thin Wrappers

🎯 User Experience Goals

Platform and Rendering Philosophy

EAGL prioritises desktop OpenGL capabilities to maximise educational and practical value for graphics programming:

Desktop-First Approach

Cross-Platform Asset Compatibility

Educational Mission

Multi-Platform Strategy

Submitting Changes

  1. Create a feature branch: git checkout -b feature/descriptive-name
  2. Make your changes following the style guidelines above
  3. Add or update tests for your changes
  4. Run the full test suite: mix test
  5. Update documentation if you've added new features
  6. Commit with clear messages: Use present tense, describe what the commit does
  7. Push your branch: git push origin feature/descriptive-name
  8. Open a Pull Request with:

Questions and Support

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments