README
View SourceEAGL

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:
- Make it easier to translate OpenGL tutorials and examples from resources like Learn OpenGL into Elixir.
- Provide basic helper functions to bridge the gap between idiomatic Elixir and OpenGL's state machine, using the Wings 3D Erlang source as a guide to prescriptive vs helpful additions
- Enable other libraries and apps to build on this one and libraries like ECSx and the list at Awesome Elixir Gaming
The following are non-goals:
- Focussing on 2D GPU graphics (see Scenic for that)
- Wrapping of the Erlang wx library
- A Shader DSL
- A UI layout/component library
- 3D mesh modelling (leave that to Wings 3D, Blender etc)
Quick Start
# Add to mix.exs
{:eagl, "~> 0.5.0"}
EAGL includes several examples to demonstrate its capabilities. Use the unified examples runner:
./priv/scripts/run_examples
════════════════════════════════════════════════════════════════
EAGL Examples Menu
════════════════════════════════════════════════════════════════
Non-LearnOpenGL Examples:
01) Math Example - Comprehensive EAGL.Math functionality demo
02) Teapot Example - 3D teapot with Phong shading
LearnOpenGL 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
Missing: 151) 5.1 Transformations 161) 6.1 Coordinate Systems 171) 7.1 Camera
════════════════════════════════════════════════════════════════
Enter code (01, 02, 111-146), 'q' to quit, 'r' to refresh:
>
Usage
Math Operations
EAGL provides a comprehensive 3D math library based on GLM supporting:
- Vectors: 2D, 3D, 4D vector operations with constructor macros
- Matrices: 2x2, 3x3, 4x4 matrix operations with transformation functions
- Quaternions: Rotation representation, SLERP, and conversion functions
- Utilities: Trigonometry, interpolation, clamping, and geometric functions
- OpenGL Integration: All functions work with the tuple-in-list format required by Erlang's OpenGL bindings
- Sigils: Compile-time validated literals for matrices (
~m
), vertices (~v
), and indices (~i
)
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 10.0 # Translation X
0.0 1.0 0.0 20.0 # Translation Y
0.0 0.0 1.0 30.0 # Translation Z
0.0 0.0 0.0 1.0
"""
# 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)
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:
vec2/3/4
→glUniform2f/3f/4f
mat2/3/4
→glUniformMatrix2fv/3fv/4fv
- Numbers →
glUniform1f/1i
- Booleans →
glUniform1i
(0 or 1)
import EAGL.Shader
# Compile and link shaders
{: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:
- Image Loading:
load_texture_from_file()
with automatic fallback to checkerboard patterns - Texture Creation:
create_texture()
returns{:ok, id}
tuples for error handling - Parameter Setting:
set_texture_parameters()
converts atoms to OpenGL constants - Data Loading:
load_texture_data()
handles format/type conversion with defaults - Procedural Textures:
create_checkerboard_texture()
generates test patterns - Graceful Degradation: Helpful warnings when optional dependencies aren't available
- Direct OpenGL: Use
:gl
functions directly for binding, mipmaps, and cleanup
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 atom-to-constant conversion
set_texture_parameters(
wrap_s: :repeat,
wrap_t: :repeat,
min_filter: :linear_mipmap_linear,
mag_filter: :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
Currently we only support the .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)
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(), # location: 0, size: 3, stride: 24, offset: 0
color_attribute(stride: 24, offset: 12) # location: 1, size: 3, stride: 24, offset: 12
]
{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
# 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:
position_attribute()
- 3 floats (x, y, z) at location 0color_attribute()
- 3 floats (r, g, b) at location 1texture_coordinate_attribute()
- 2 floats (s, t) at location 2normal_attribute()
- 3 floats (nx, ny, nz) at location 3
Key Benefits:
- Automatic calculation:
vertex_attributes()
eliminates manual stride/offset math - Type safety: Compile-time checks for attribute configuration
- Standard patterns: Common attribute layouts are pre-defined
- Flexible: Mix automatic and manual configuration as needed
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:
- Default Size: 1024x768 pixels (can be customized with
size:
option) - 2D Rendering (default): No depth buffer, suitable for triangles, sprites, UI elements
- 3D Rendering: Enables depth testing and depth buffer for proper 3D scene rendering
- Automatic ENTER Handling: Optional ENTER key handling for simple examples and tutorials
- Tick Events: Automatic 60 FPS tick events for animations and updates (optional
handle_event/2
callback)
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", return_to_exit: true)
# Custom window size and options
EAGL.Window.run(__MODULE__, "Custom Size App", size: {1280, 720}, depth_testing: true, return_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 tick events for animations (60 FPS)
@impl true
def handle_event(:tick, state) do
# Update animations, physics, etc.
{:ok, updated_state}
end
end
Requirements
- Elixir: 1.14 or later
- Erlang/OTP: 25 or later (with wx support - included in standard distributions)
- OpenGL: 3.3 or later (for modern shader support)
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
macOS
OpenGL is included with macOS. No additional setup required.
Note: 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.
Windows
OpenGL is typically available through graphics drivers. If you encounter issues, ensure your graphics drivers are up to date.
Installation
Clone the repository:
git clone https://github.com/yourusername/eagl.git cd eagl
Install dependencies:
mix deps.get
Compile the project:
mix compile
Run tests to verify everything works:
mix test
Try the examples:
./priv/scripts/run_examples
Project Structure
lib/
├── eagl/ # Core EAGL modules
│ ├── buffer.ex # VAO/VBO helper functions
│ ├── const.ex # OpenGL constants
│ ├── error.ex # Error checking and reporting
│ ├── math.ex # GLM-style math library
│ ├── model.ex # 3D model management
│ ├── obj_loader.ex # Wavefront OBJ parser
│ ├── shader.ex # Shader compilation
│ ├── window.ex # Window management
│ └── window_behaviour.ex # Window callback behavior
├── examples/ # Example applications
│ ├── math_example.ex # Math library demonstrations
│ ├── teapot_example.ex # 3D teapot rendering
│ └── learnopengl/ # LearnOpenGL tutorial ports
└── wx/ # wxWidgets constants
test/
├── eagl/ # Unit tests for EAGL modules
└── eagl_test.exs # Integration tests
priv/
├── models/ # 3D model files (.obj)
├── scripts/ # Convenience scripts
│ └── run_examples # Unified examples runner
└── shaders/ # GLSL shader files
└── learnopengl/ # LearnOpenGL tutorial shaders
Features
- ✅ Shader Management: Automatic compilation, linking, and error reporting
- ✅ Texture Management: Comprehensive texture creation, configuration, and loading
- ✅ 3D Model Loading: Wavefront OBJ format with normals and texture coordinates
- ✅ Math Library: GLM-compatible vectors, matrices, quaternions with full OpenGL integration
- ✅ Buffer Helpers: Wings3D-inspired VAO/VBO management functions
- ✅ Error Handling: Comprehensive OpenGL error checking and reporting
- ✅ Window Management: Cross-platform window creation with wxWidgets
- ✅ Event Handling: Resize, close, paint, and 60 FPS tick events
- ✅ Resource Cleanup: Automatic cleanup of OpenGL resources
- ✅ LearnOpenGL Examples: Partial "Getting Started" series - direct ports of OpenGL tutorials
- ✅ Testing: Full test suite with OpenGL context mocking
Roadmap
The current focus is to:
- [ ] In Progress: Complete the "Getting Started" LearnOpenGL examples series
- ✅ Hello Window (1.1-1.2): 2 examples
- ✅ Hello Triangle (2.1-2.5): 5 examples
- ✅ Shaders (3.1-3.6): 6 examples
- ✅ Textures (4.1-4.6): 6 examples
- [ ] Missing: Transformations (5.1): 1 example needed
- [ ] Missing: Coordinate Systems (6.1): 1 example needed
- [ ] Missing: Camera (7.1): 1 example needed
- [ ] Continue with "Lighting" chapter examples
- [ ] Load common model types like GLTF
And in future:
- [ ] Be able to apply post-processing effects
- [ ] More extensive camera/lighting/material helpers
- [ ] Access to a physics engine
- [ ] Built-in GPU profiling tools
Troubleshooting
Common Issues
Interactive Examples Not Responding
Examples require user interaction (ENTER key to exit). This can cause issues during testing:
# Run only unit tests, excluding interactive examples
mix test test/eagl/ --exclude interactive
# Set a timeout for interactive tests
mix test --timeout 10000
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
Interactive examples wait for user input and will timeout in continuous integration:
- Examples are tagged with
@tag :interactive
- CI environments automatically exclude these tests
- Run interactive tests individually during local development
Platform-Specific Issues
OpenGL Context Creation Failures
If you encounter context creation errors:
- Linux: Ensure mesa development packages are installed
- macOS: Update to a supported macOS version (10.9+)
- Windows: Update graphics drivers
Missing Dependencies
If optional dependencies are missing, EAGL will show warnings but continue with fallback behaviour:
- Image loading falls back to procedural textures
- Missing models show error messages but don't crash
Contributing
We welcome contributions. Suggested contributions include:
- LearnOpenGL tutorial ports: Help complete the tutorial series
- Documentation improvements: Examples, tutorials, API documentation
- Platform-specific optimisations: Performance or compatibility improvements
- Example applications: Links to demo projects showcasing EAGL capabilities
- Bug fixes: Issues with existing functionality
- Testing improvements: Better mocks, integration tests, or test utilities
Please read through these guidelines before submitting changes.
Development Setup
- Fork and clone the repository
- Install dependencies:
mix deps.get
- Run tests to ensure everything works:
mix test
- Try the examples:
./priv/scripts/run_examples
Code Standards
Style Guidelines
- Follow standard Elixir formatting (
mix format
) except keep matricies in tabular format - Use descriptive variable names, especially for OpenGL state
- Include typespecs for public functions
- Document complex algorithms and OpenGL-specific concepts
Testing Requirements
- Add tests for new functionality
- Ensure existing tests pass:
mix test
- Tag interactive tests with
@tag :interactive
- Mock OpenGL calls in unit tests where possible
Documentation Standards
- Update README.md for new features
- Add docstrings for public functions
- Include code examples in documentation
- Our tone is calm, concise and factual e.g. avoid 'sales' language and over-use of '!'
- Write in Australian/British English for documentation, US English for code
Design Philosophy
EAGL focuses on meaningful abstractions rather than thin wrappers around OpenGL calls:
✅ Provide Value
- Error handling:
{:ok, result}
tuples and comprehensive error checking - Type safety: Atoms to OpenGL constants (
wrap_s: :repeat
) - Sensible defaults: Reduce boilerplate with common parameter combinations
- Complex operations: Multi-step procedures like shader compilation and linking
- Data transformations: Converting Elixir structures to OpenGL formats
- Testing utilities: Procedural textures and geometry for development
❌ Avoid Thin Wrappers
- Simple OpenGL calls: Use
:gl.bindTexture()
,:gl.generateMipmap()
directly - One-line functions: Don't wrap functions that only add
check()
calls - State management: Let users manage OpenGL state explicitly when appropriate
🎯 User Experience Goals
- Selective imports:
import EAGL.Error
for explicit error checking - Direct OpenGL access: When EAGL doesn't add substantial value
- Direct OpenGL integration: Mix EAGL helpers with direct OpenGL calls
Submitting Changes
- Create a feature branch:
git checkout -b feature/descriptive-name
- Make your changes following the style guidelines above
- Add or update tests for your changes
- Run the full test suite:
mix test
- Update documentation if you've added new features
- Commit with clear messages: Use present tense, describe what the commit does
- Push your branch:
git push origin feature/descriptive-name
- Open a Pull Request with:
- Clear description of the changes
- Reference to any related issues
- Screenshots for visual changes
- Test results if applicable
Questions and Support
- Issues: Use GitHub issues for bugs and feature requests
- Discussions: Use GitHub discussions for questions and design discussions
- Examples: Look at existing code in
lib/examples/
for patterns
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Learn OpenGL for excellent OpenGL book and tutorial code. If the examples helped you understand OpenGL better please consider a donation to the author, Joey De Vries.
- Wings3D for inspiration and helper function patterns - the name EAGL(e) is a tip of the hat to this project
- The Erlang/OTP team and particularly Dan Gudmundsson for the wxWidgets bindings
- The local Elixir User Group for putting up with my occasional random talks
- Cursor and Claude Sonnet for giving me the patience to get to running code and porting Joey's Learning OpenGL examples