<div align="center">
<h1>EAGL</h1>
<img src="assets/eagl_logo_grey_on_transparent.png" alt="EAGL Logo" title="EAGL Logo" width="200">
<p>
Make it EAsier to work<br>
with OpenGL in Elixir.
</p>
</div>
## 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](https://learnopengl.com) 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](https://github.com/ecsx-framework/ECSx) and the list at [Awesome Elixir Gaming](https://github.com/njwest/Awesome-Elixir-Gaming)
The following are non-goals:
- Focussing on 2D GPU graphics (see [Scenic](https://github.com/ScenicFramework/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
```elixir
# 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:
```elixir
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
```elixir
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)
```elixir
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
```elixir
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.
```elixir
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.
```elixir
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 0
- `color_attribute()` - 3 floats (r, g, b) at location 1
- `texture_coordinate_attribute()` - 2 floats (s, t) at location 2
- `normal_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
```elixir
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)
```elixir
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:
```bash
# 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
1. Clone the repository:
```bash
git clone https://github.com/yourusername/eagl.git
cd eagl
```
2. Install dependencies:
```bash
mix deps.get
```
3. Compile the project:
```bash
mix compile
```
4. Run tests to verify everything works:
```bash
mix test
```
5. Try the examples:
```bash
./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:
```bash
# 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
1. Fork and clone the repository
2. Install dependencies: `mix deps.get`
3. Run tests to ensure everything works: `mix test`
4. 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
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:
- 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](LICENSE) file for details.
## Acknowledgments
- [Learn OpenGL](https://learnopengl.com) for excellent OpenGL book and tutorial code. If the examples helped you understand OpenGL better please consider a [donation to the author](https://www.paypal.me/learnopengl/), Joey De Vries.
- [Wings3D](https://wings3d.com) 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](https://https://elixir.sydney) for putting up with my occasional random talks
- [Cursor](https://cursor.com) and Claude Sonnet for giving me the patience to get to running code and porting Joey's Learning OpenGL examples