Lightweight OpenGL rendering engine. Basic abstraction layer for compositional GLSL shader programming and rendering.
luarocks install four
Four is a lightweight OpenGL rendering engine for Lubyk. It provides a basic abstraction layer for compositional GLSL shader programming and rendering.
Below, a quick tour of four is provided, more details can be found in the library's reference documentation generated from the Lua files. Basic knowledge of OpenGL and 3D real-time rendering is assumed.
Four works under an implicit OpenGL rendering context. Setting up the
OpenGL context is left to the client of the library. Currently it
needs at least an OpenGL 3.2 context. New rendering backends
(e.g. OpenGL ES) can be added by implementing the Renderer.lua
interface; RendererGL32.lua can be used as a blueprint.
To get something rendered on the screen the following steps must be taken.
- Create a renderer object
rendererwith the appropriate backend (defaults to OpenGL 3.2). - Make sure there's a corresponding valid OpenGL context.
- Invoke
renderer:render(cam, {obj})wherecamis a camera object andobja renderable.
A renderable is any object with two mandatory fields, geometry and
effect respectively referencing a Geometry and Effect object;
more on this in the following sections.
A minimal example drawing a tri-colored triangle can be found in
test/minimal.lua, run it with:
luajit minimal.lua
The following conventions are used throughout the library.
- In 3D space we assume a right-handed coordinate system.
- Angles are always given in radians.
- In 2D space positive angles determine counter clockwise rotations.
- In 3D space positive angles determine rotations directed according to the right-hand rule.
Four provides data types and functions for vectors V2, V3, V4,
4x4 matrices M4, and quaternions Quat. Even though these types are
implemented as Lua arrays use them as abstract, immutable, types.
There is no special type for color values but the Color module
provides convenience constructor and accessors to specify colors in
the HSVA or RGBA color spaces. The resulting colors are stored as RGBA
intensities in V4 values.
A Transform object is a convenience mutable object to orient an
object in 3D space. It decomposes an M4 matrix into scaling,
rotation and translation components (applied in this order) available
through these keys:
scale, theV3value defining the scaling component.rot, theQuatvalue defining the orientation component.pos, theV3value defining the translation component.matrix, theM4matrix resulting from scaling, rotating and translating according toscale,rotandpos.
The scale, rot and pos keys can be set directly; this
automatically updates matrix and vice-versa.
Transform objects can be attached to renderables and cameras via their
transform key.
A Buffer object holds 1D to 4D integer or float vectors in a linear
Lua array (future versions of four should also allow to wrap a
malloc'd C pointer).
Buffers are used to specify vertex data and texture data. The important
Buffer keys are:
dim, the vector dimension.scalar_type, the vector's element type, defines how the data will be stored on the GPU.
The following code defines a buffer with three 3D vertices.
local vs = four.Buffer { dim = 3, scalar_type = four.Buffer.FLOAT }
vs:push3D(-0.8, -0.8, 0.0)
vs:push3D( 0.8, -0.8, 0.0)
vs:pushV3(four.V3(0.0, 0.8, 0.0))
assert(vs:length() == 3)
assert(vs:scalar_length() == 9)By default a buffer's data is disposed once it is uploaded on the GPU
by the renderer. This can be prevented by setting the disposable key
to false. In that case also consider setting the update key to an
appropriate value.
WARNING Buffer indexing is one-based for now. Four will rapidly change to zero-based indexing for buffers because OpenGL indexes are zero-based and having to deal with the two forms simultaneously is error-prone.
A Geometry object gathers vertex data buffers, an index buffer and a
primitive --- lines, triangles, triangle strips, etc.
The index buffer indexes with zero-based indices into vertex data buffers. Along with the primitive this specifies renderable geometry for the GPU.
The important Geometry keys are:
data, table of named buffer objects all of the same length defining per vertex data. The key names are used to bind to the corresponding vertex shader inputs.primitive, indicates how the index buffer should be interpreted to specify the geometry. For example withGeometry.TRIANGLES, the indices in theindexbuffer are taken three by three to define triangles.index, a buffer of unsigned ints or bytes of any dimension, indexing with zero-based indices into the buffers ofdatato define the actual sequence of primitives.
The following function returns a Geometry object for a colored
triangle located in clip space:
function triangle () -- Geometry object for a triangle inside clip space
local vs = Buffer { dim = 3, scalar_type = Buffer.FLOAT }
local cs = Buffer { dim = 4, scalar_type = Buffer.FLOAT }
local is = Buffer { dim = 3, scalar_type = Buffer.UNSIGNED_INT }
vs:push3D(-0.8, -0.8, 0.0) -- Vertices
vs:push3D( 0.8, -0.8, 0.0)
vs:push3D( 0.0, 0.8, 0.0)
cs:pushV4(four.Color.red ()) -- Vertices' colors
cs:pushV4(four.Color.green ())
cs:pushV4(four.Color.blue ())
is:push3D(0, 1, 2) -- Index for a single triangle
return Geometry { primitive = Geometry.TRIANGLES,
index = is, data = { vertex = vs, color = cs}}
endTo access the geometry's data in a vertex shader the GLSL code must
declare variables whose names match the keys of the data table:
in vec3 vertex; // vertex buffer
in vec3 color; // color bufferNote that once a geometry object was rendered its buffer structure --- that is the buffer objects used --- cannot change, the underlying data in the buffers may, however, change.
An effect object defines a configuration of the GPU for rendering a geometry object. The important keys of an effect are:
vertex, the vertex shader.geometry, the geometry shader (optional).fragment, the fragment shader.default_uniforms, a key/value table defining default values for uniforms.uniforms, a uniform lookup function invoked to get uniform values.
Shaders fields vertex, geometry and fragment are either
Effect.Shader objects or a list thereof. An Effect.Shader object
only wraps a piece of GLSL code, for example Effect.Shader(src) is a
shader object with the GLSL code src.
The following effect colors geometry specified in clip space.
local effect = Effect -- Colors the triangle
{
vertex = Effect.Shader [[
in vec3 vertex;
in vec3 color;
out vec4 v_color;
void main()
{
v_color = vec4(color, 1.0);
gl_Position = vec4(vertex, 1.0);
}]],
fragment = Effect.Shader [[
in vec4 v_color;
out vec4 color;
void main() { color = v_color; }
]]
}If a shader field holds a table of shader objects, those are concatenated to form the shader source, this allows to define and reuse shader functions from other modules.
Shaders may declare uniforms variables. When an effect e is used the
actual value bound to the uniform is determined as follows. Given an
uniform named u:
uniform vec4 u;the function call e.uniform(e, cam, r, "u") is invoked where e is
the effect, cam the current camera, r the renderable. If the
function returns nil, the result of e.default_uniforms["u"] is
used.
The default implementation of e.uniform is return r["u"], that is
it looks for a corresponding key in the renderable.
TODO document the map from lua types to GLSL types.
The effect module defines a few special uniform values. These values are dynamically computed by the renderer according to the current camera and renderable (or other parameters).
For example the special uniform value Effect.MODEL_TO_CLIP
automatically holds the matrix for transforming from model space to
clip space according to the current renderable transform, camera
transform and projection.
Here is a typical vertex shader transforming vertices from model space to clip space:
local effect = Effect
{
default_uniforms = { m2c = Effect.MODEL_TO_CLIP }
vertex = Effect.Shader [[
uniform mat4 m2c;
in vec3 vertex;
void main () { gl_Position = m2c * vec4(vertex, 1.0); }
]]
}A renderable can be any object. To be rendered by the renderer it must
at least have an effect and a geometry object, otherwise it is
simply ignored. The following keys are interpreted by the renderer:
geometry, the geometry object to render.effect, the effect with whichgeometryshould be rendered.transform(optional), a transform object defining a transform to apply togeometry, in other words, a world transform.instance_count, the number of instances to render.visible(optional), if present andfalsedisables the rendering of the renderable.
A Camera object defines a view volume of world space. Only those
object that are part of the view volume are rendered. The important
keys:
transformdefines the location and orientation of the camera. The default transform lies at the origin and looks down the z-axis.range, aV2value defining the near and far clip plane as a distance along the forward vector from the current point of view.fov, the horizontal field of view.aspect, the camera width/height ratio.
The following code defines a camera located at cam_pos and looking
at the point cam_target.
local cam_pos = V3(-3, 5, 5)
local cam_target = V3(1, 1, 1)
local cam_rot = Quat.rotMap(-V3.oz(), V3.unit(cam_target - cam_pos))
local cam = Camera { transform = Transform { pos = cam_pos, rot = cam_rot }}TODO implement and use lookat.
Given a renderer, a camera cam and a list of renderables rs, a frame
is rendered with:
renderer:render(cam, rs) To minimize GPU configuration changes, the renderables in rs are sorted
and rendered according to the effect they use.
Renderables can be rendered with multiple pass. In fact an effect can be either an effect as defined above or a list of effects. In general this results in a tree structure. The depth-first order traversal of this tree defines a number of passes. The renderer start by rendering the first pass of each renderable, then the second pass etc.
In each pass effects are partitionned into opaque and non-opaque
effects according to the opaque boolean attribute of an effect.
Renderables with opaque effects are rendered before the ones
with non-opaque effects.
TODO examples
TODO expand and improve
For a renderable r verify the following
assert(r.visible == nil or r.visible)assert(r.effect)assert(r.geometry)assert(r.geometry.index.type == UNSIGNED_INT || r.geometry.index.type == UNSIGNED_BYTE)
Ensure that geometry.primitive, buffer.type is not nil.
