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)
# 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:
>
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
- Coordinate System: Consistent right-handed coordinate system for proper OpenGL compatibility
- GLM Compatibility:
mat4_look_atnow exactly matches GLM'slookAtRHmatrix layout (fixed in v0.8.0) - Sigils: Compile-time validated literals for matrices (
~m), vertices (~v), and indices (~i)
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
"""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)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:
- Orbit: Left-drag to rotate around the target
- Zoom: Scroll to move closer or further
- Pan: Middle-drag to shift the target point
- Fit to bounds:
fit_to_scene/1orfit_to_bounds/2for automatic framing
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.
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/4fmat2/3/4→glUniformMatrix2fv/3fv/4fv- Numbers →
glUniform1f/1i - Booleans →
glUniform1i(0 or 1)
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)
])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 - Type-Safe Parameters:
set_texture_parameters()with compile-time validated options - 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
:glfunctions 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 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])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)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.
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.
- Complete Property Support: All GLTF 2.0 properties from section 5 of the specification
- Type Safety: Elixir structs with proper type specifications for all properties
- Extensions Support: Built-in extensions mechanism with validation
- PBR Materials: Full physically-based rendering material support
- Animations: Keyframe animations with multiple interpolation modes
- Skinning: Vertex skinning with joint hierarchies
- Multiple Cameras: Perspective and orthographic camera types
- Texture Management: Complete texture pipeline with samplers and filtering
- Buffer Views: Efficient binary data management with accessors
- Scene Graphs: Hierarchical node structures with transformations
- Validation: Built-in validation functions for document integrity
# 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)}")
endThe 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
- Specification Compliance: Strict adherence to GLTF 2.0 specification
- Elixir Idiomatic: Uses Elixir conventions and patterns
- Type Safety: Comprehensive type specifications and validation
- Extensibility: Support for GLTF extensions mechanism
- Performance: Efficient structures for runtime use
- GLB Support: Binary GLTF container format loading and parsing
- Integration: GLTF.EAGL bridge module for scene graph and VAO creation
- Validation: Structure validation with index and extension checking
- JSON Serialization: Import/export to GLTF JSON format (non-binary)
- Extensions: Built-in support for common GLTF extensions
- Multi-primitive meshes: Support meshes with more than one primitive
- Buffer view stride: Support strided buffer views for interleaved data
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.
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 arraysStandard Attribute Helpers:
position_attribute()- 3 floats (x, y, z), defaults to location 0 but can be overriddencolor_attribute()- 3 floats (r, g, b), defaults to location 1 but can be overriddentexture_coordinate_attribute()- 2 floats (s, t), defaults to location 2 but can be overriddennormal_attribute()- 3 floats (nx, ny, nz), defaults to location 3 but can be overridden
Two Configuration Approaches:
- Automatic Layout (recommended):
vertex_attributes()assigns sequential locations (0, 1, 2, 3...) and calculates stride/offset automatically - Manual Layout: Individual attribute helpers allow custom locations, stride, and offset for non-standard layouts
Key Benefits:
- Flexible locations: Default locations can be overridden with
location:option - Automatic calculation:
vertex_attributes()eliminates manual stride/offset math for standard layouts - Type safety: Compile-time checks for attribute configuration
- Mix approaches: Use automatic layout for common cases, manual for custom requirements
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 foundEAGL 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
- Comprehensive Input: Full keyboard, mouse movement, mouse buttons, and scroll wheel support
- 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/2callback) - Mouse Capture: Cursor hiding and capture for first-person camera controls
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- 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)
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.
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-develEAGL 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:
- Input lag: Mouse orbit/pan and scroll may feel sluggish, especially at high resolutions
- Lower frame rates: Adaptive tick timing helps, but expect lower FPS than native Linux or Windows
- Stilted interaction: Large windows can amplify the lag
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.
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.
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:
- Examples fail with
{:nif_not_loaded, :module, :gl, :line, 356}or similar errors wxmodule loads successfully but OpenGL calls fail- Error occurs immediately when trying to use any
:gl.* functions
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-versionsRecommended version combinations:
- Erlang/OTP 26.2.1 + Elixir 1.15.7-otp-26
- Erlang/OTP 25.3 + Elixir 1.14.5-otp-25
EAGL automatically handles retina display scaling on macOS. The viewport will correctly fill the entire window regardless of display pixel density.
How it works:
- Logical size: What you see (e.g., 1024×768)
- Physical size: Actual pixels (e.g., 2048×1536 on 2× retina)
- Automatic scaling: EAGL detects the content scale factor and passes physical dimensions to render functions
What this means:
- ✅ Viewport fills entire window on retina displays
- ✅ Text and graphics appear crisp at native resolution
- ✅ No manual scaling required in your render functions
- ✅ Works seamlessly across different display types
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.
OpenGL is typically available through graphics drivers. If you encounter issues, ensure your graphics drivers are up to date.
- 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:
mix exampleslib/
├── 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
- ✅ Camera System: Orbit camera for model viewing (LearnOpenGL examples include first-person camera)
- ✅ Shader Management: Automatic compilation, linking, and error reporting
- ✅ Texture Management: Comprehensive texture creation, configuration, and loading
- ✅ 3D Model Loading: Wavefront OBJ and glTF 2.0 (GLB) formats with scene graph support
- ✅ 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
- ✅ Scene Graph: Hierarchical rendering with transform propagation
- ✅ Animation: Keyframe animation with timeline playback and glTF integration
- ✅ Event Handling: Keyboard, mouse buttons, scroll wheel, resize, close, and adaptive tick events
- ✅ Resource Cleanup: Automatic cleanup of OpenGL resources
- ✅ LearnOpenGL Examples: Complete "Getting Started" series - direct ports of OpenGL tutorials
- ✅ GLTF Examples: 5 progressive examples from simple box to PBR helmet
- ✅ Testing: Full test suite with OpenGL context mocking (469 tests)
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
- ✅ Transformations (5.1-5.2): 3 examples
- ✅ Coordinate Systems (6.1-6.4): 4 examples
- ✅ Camera (7.1-7.6): 6 examples completed
- Continue with "Lighting" chapter examples
- Load glTF 2.0 models via GLTF.EAGL bridge (GLB format, PBR materials, scene graphs)
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
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.exsIf 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.
Examples now use automatic timeouts and run successfully in continuous integration environments:
- Examples accept a
timeout:option for automated testing - CI environments run examples with 500ms timeouts
- Examples exit cleanly after timeout with proper resource cleanup
- No manual interaction required
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
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
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.
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.samplesThis 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.
If GLTF examples close immediately on macOS while the basic Box example works:
-
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. -
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.
We welcome contributions. Suggested contributions include:
- LearnOpenGL tutorial ports: Help correct the tutorial series (I will do the initial ports)
- Documentation improvements: Examples, 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.
- Fork and clone the repository
- Install dependencies:
mix deps.get - Run tests to ensure everything works:
mix test - Download GLB samples for GLTF examples:
mix glb.samples - Try the examples:
mix examples
- Follow standard Elixir formatting (
mix format) but... - Keep matricies in tabular format and wrap with
# mix format: off|on - Use the
~matrix,~vertex and~index sigils for compile time constants - Use descriptive variable names, especially for OpenGL state
- Include typespecs for public functions
- Document complex algorithms and OpenGL-specific concepts
- Add tests for new functionality
- Ensure existing tests pass:
mix test - Update examples to accept
optsparameter for timeout testing - Mock OpenGL calls in unit tests where possible
- 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
EAGL focuses on meaningful abstractions rather than thin wrappers around OpenGL calls:
- Error handling:
{:ok, result}tuples and comprehensive error checking - Type safety: Compile-time validation and clear parameter names (
wrap_s: @gl_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
- 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
- Selective imports:
import EAGL.Errorfor explicit error checking - Direct OpenGL access: When EAGL doesn't add substantial value
- Direct OpenGL integration: Mix EAGL helpers with direct OpenGL calls
EAGL prioritises desktop OpenGL capabilities to maximise educational and practical value for graphics programming:
- Full OpenGL Access: Modern OpenGL 3.3+ features including geometry shaders, tessellation, compute shaders
- Educational Completeness: Learn comprehensive graphics techniques without platform constraints
- Professional Preparation: Develop skills that transfer directly to industry graphics programming
- Research Capabilities: Support advanced techniques needed in graphics research and development
- glTF 2.0 Integration: Runtime-neutral asset format for broad ecosystem compatibility
- Asset Bridge: Import glTF models and scenes for use with full desktop OpenGL capabilities
- Separation of Concerns: Asset format (glTF) independent from rendering platform (desktop OpenGL)
- Ecosystem Integration: Assets work across different renderers while maintaining access to advanced features
- Format-Agnostic Runtime: EAGL uses glTF as its reference format but keeps parallel concepts—
GLTF.*modules represent the glTF schema,EAGL.*modules provide format-agnostic runtime structures (Scene, Node, Camera, etc.). The bridge converts glTF into EAGL; other formats (OBJ, FBX) would use their own bridges into the same EAGL types, keeping EAGL decoupled from any single format.
- Complete Feature Set: Access to the full spectrum of modern OpenGL techniques
- Comprehensive Learning: Explore the complete range of 3D graphics development approaches
- Skill Transfer: Techniques applicable to game engines, CAD software, scientific visualisation
- Different Design Goals: Desktop and web platforms serve different needs - we focus on desktop's strengths
- Asset Compatibility: Use glTF for models that work across rendering platforms
- Complementary Ecosystems: Desktop development for full capabilities, established tools for web deployment
- Platform Strengths: Leverage desktop OpenGL's comprehensive feature set where appropriate
- Standards-Based: Integration through established formats rather than platform compromises
- 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
- 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
This project is licensed under the MIT License - see the LICENSE file for details.
- 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 Anthropic for giving me the patience to get to running code, making sense of GLTF and porting Joey's Learning OpenGL examples




