jit.gl.lua
jit.gl.lua embeds the Lua scripting language inside a Jitter OpenGL object. jit.gl.lua serves as both a general purpose Lua scripting object and a 3D graphics scripting object. It is very similar to the js object for JavaScript with the addition that OpenGL commands can be used directly. OpenGL is what Jitter uses for 3D graphics rendering. Normally, access to the 3D drawing functionality is wrapped in higher level jit.gl objects (e.g. jit.gl.gridshape). jit.gl.lua however exposes OpenGL directly in its scripting environment for more precise, dynamic control over OpenGL's 3D drawing commands.
The Lua scripting environment inside jit.gl.lua interacts with Max and Jitter in two ways: through patcher messages and through commands from Jitter's OpenGL rendering system. If you have done Lua scripting in other contexts, that knowledge is directly applicable to working with jit.gl.lua. jit.gl.lua simply adds extra functionality to the standard Lua environment, and this is what is covered in this documentation. For more information on Lua, visit the Lua homepage.
For documentation on the OpenGL commands, what they do, and how to use them, please visit the OpenGL website. Of particular note are the OpenGL man pages, which are available for each command. They explain what type of arguments a command requires, what it does, and any possible errors that may arise from improper use.
The 'this' Variable
In jit.gl.lua, there is a special variable called this
. this
represents the jit.gl.lua object the Lua script is running inside. Whenever you need to modify or query the properties of embedding jit.gl.lua object, use the this
variable. A common idiom when scripting with jit.gl.lua is to use the drawto attribute of jit.gl.lua when creating other jit.gl objects. All jit.gl objects must belong to a rendering context in order to be used. In Jitter, rendering contexts are given names, so jit.gl objects join a rendering context using the context's name. This attribute is called drawto
.
When other jit.gl objects are created in a script, they too have to belong to a context, so we typically attach them to the same context as the jit.gl.lua object, which can be accessed by referring to this.drawto
.
Messages and Functions
Global functions in jit.gl.lua can be called by sending messages to the jit.gl.lua object. If a script contains the code
function apply()
print( "the apply() function was called" )
end
sending the message apply to a jit.gl.lua object will call the function above.
If the message is followed by any extra strings or numbers, these will be passed as arguments to the function. While a function can have any name and be called by a message, there are a few function names that have special significance. There are two groups of these functions: one for handling patcher input and another for Jitter OpenGL messages.## Patcher Interaction Functions
The patcher handling, the functions are float
, int
, list
, loadbang
, closebang
, and scriptload
.
function float(v)
print( "float" , v, "inlet" , this.last_inlet)
end
function int(v)
print( "int" , v, "inlet" , this.last_inlet)
end
function list(...)
local values = {...}
print( "list" , table.concat(values, ", " ), "inlet" , this.last_inlet)
end
When a number is sent to a jit.gl.lua inlet, the float
messages is called and passed the value. When an integer is sent, the int
message is called. Similarly, a list of values triggers the list
function with the elements of the list set as arguments to the function. The list can be captured into a table by using the Lua idiom
-- variable arguments captured into a table
local thelist = {...}
When the patcher is loaded, the loadbang
function is called, and when it is closed, the closebang
function is called. loadbang
will only be called when the patcher containing the jit.gl.lua object is called. This happens once during the patcher lifetime, so if a script is reloaded, loadbang
will not be called again. To perform any setup each time a script is loaded, use the scriptload
function.
OpenGL Callback Functions
The Opengl methods are draw
, dest_changed
, dest_closing
. They are called in response to events in the Jitter OpenGL rendering system. The OpenGL callback functions are used to manage OpenGL resources and draw to the rendering destination. It is important to remember that OpenGL commands can only be used inside the three OpenGL callback functions.
Whenever a jit.gl.lua object is asked to draw itself by the jit.gl.render object, it calls the draw
function. When the draw
function is called OpenGL commands can be used to draw to the rendering destination. Any OpenGL related attributes set on the jit.gl.lua object objet such as blend
will be applied before the draw function is called, so they can be used initialize the drawing state before any OpenGL commands are called in the draw function.
The dest_changed
and dest_closing
functions on the other hand are used to manage OpenGL resources. dest_changed
is called when the OpenGL context is created and the first frame is about to be rendered. dest_closing
is called when the context is about to be destroyed. When both functions are called, the OpenGL context is active so any OpenGL command can be used. Typically the dest_changed
function is used to create OpenGL resources such as display lists while the dest_closing
function is used to destroy them.
local gl = require("opengl")
local GL = gl
function dest_changed()
-- context is new
end
function dest_closing()
-- context is closing
end
function draw()
-- draw a line
gl.Begin(GL.LINES)
gl.Vertex( -1 , 0 , 0 )
gl.Vertex( 1 , 0 , 0 )
gl.End()
end
Creating Inlets and Outlets
jit.gl.lua can have a variable number of inlets and outlets. The number of inlets and outlets are specified by setting the inlets
and outlets
attriubutes respectively on the jit.gl.lua object itself.
-- 3 inlets, 2 outlets
this.inlets = 3
this.outlets = 2
Since jit.gl.lua has dynamic inlets and outlets, whenever the inlets
or outlets
attriubutes are set, the change is immediately reflected in the patcher.
Inlet Detection and Data Conversion
Lua has a single number type, so integer and float inputs are both converted to a Lua number. When an input is sent to an inlet, jit.gl.lua 's last_inlet
attribute is set, allowing a script to determine what inlet a message was sent to so that inlet-specific behaviors can be designed if needed.
In the script above, the last_inlet
attribute of jit.gl.lua is used to save the input number when it comes in the right inlet. When input comes in the left inlet, it triggers an addition operation and the result is sent out the outlet.
Outlets and Data Conversion
Outlets in jit.gl.lua are accessed through the outlet
function. outlet
takes two or more arguments. The first argument is the outlet index with 0 as the left-most outlet. The other arguments are the values to be sent out the outlet. Outlets can be sent both string and numerical values. If a numerical value is an integer, jit.gl.lua will convert it to an Max integer type, otherwise it will be a Max float type.
Creating Jitter Objects
Jitter objects can be created in a jit.gl.lua script using the jit.new
function. When called, jit.new
will create and return a Jitter object. The arguments to jit.new
are the classname of the object and any additional arguments that the object's constructor might take. For example, to create a jit.xfade object:
xfade = jit.new( "jit.xfade" )
to create a jit.gl.gridshape object:
-- classname of the object, the context name
gshape = jit.new( "jit.gl.gridshape" , this.drawto)
Once created, the messages and attributes of the object are exposed to Lua in the usual Lua fashion. In Lua, functions belonging to an object are called using the ':' operator, which is a kind of object-oriented syntax sugar. This operator passes the object itself in as an implicit first argument.
gshape = jit.new( "jit.gl.gridshape" , this.drawto)
function draw()
-- use ':' to call the gridshape's draw function
-- equivalent to gshape.draw(gshape)
gshape:draw()
end
All Jitter object messages can be called this way. Attributes are acessed more like properties stored in a table and can be accessed using the '.' operator. For example:
gshape = jit.new("jit.gl.gridshape", this.drawto)
-- set some of the gridshape's attributes
gshape.shape = "cylinder"
gshape.automatic = 0
gshape.poly_mode = { 1 , 1 } -- set an attribute that takes a list of values
-- print the shape attribute
print("gridshape using shape", gshape.shape)
Some attributes consist of a array of values such as gridshape's poly_mode
above. These can be set with a list of values stored in a table. When getting an array attribute, the list of values will be returned in a table as well.
jit.matrix
There are two ways to create a jit.matrix object in jit.gl.lua. One is using the jit.new
function as described above. The other is by using the convenience jit.matrix
function. Both are equivalent in terms of functionality. The jit.matrix
function takes the same arguments as are used when creating a jit.matrix object in a patcher. The arguments are the matrix name (optional), planecount, type, and dimensions.
-- equivalent to jit.new("jit.matrix", 4, "char", 720, 480)
mat1 = jit.matrix(4, "char" , 720, 480)
-- a named matrix
mat2 = jit.matrix( "frame" )
jit.listener
Certain Jitter objects such as jit.window send messages out an outlet in response to patcher and user events. Since Jitter objects in scripts have no outlets, this information has to be accessed another way. In jit.gl.lua, the jit.listener object is used to attach to an object and listen to any notifications it may send out.
-- listener function
function wincb(event)
print(event.subjectname)
print(table.concat(event.args, ", " ))
end
win = jit.new( "jit.window" , "x" )
-- list to the window with wincb as the callback function
listener = jit.listener(win.name, wincb)
jit.listener takes two arguments: the name of the object to listen to and the function to call when the listener gets notified of some information. The second argument can either be a function directly or the name of a global function. This code is equivalent to the code above:
function wincb(event)
print(event.subjectname)
print(table.concat(event.args, ", " ))
end
win = jit.new( "jit.window" , "x" )
listener = jit.listener(win.name, "wincb" )
Notice that the second argument in this code is a string naming the function to call as opposed to the function itself.
jit.gl Functions
In addition to the low-level OpenGL bindings discussed below, jit.gl.lua has bindings to Jitter-specific OpenGL functionality for working with Jitter textures and jit.gl objects at a lower level than usual.
-- v2 and v3 are optional
jit.gl.texcoord(v1, [v2], [v3])
-- also can take a table
jit.gl.texcoord({v1, [v2], [v3]})
jit.gl.texcoord
creates multi-texturing texture coordinates so that if there are textures bound on different units, they will each get texture coordinates to be properly displayed on the geometry.
local gl = require( "opengl" )
local GL = gl
function draw()
gl.Color( 1 , 1 , 1 , 1 )
jit.gl.bindtexture( "tex1" , 0 )
jit.gl.bindtexture( "tex2" , 1 )
gl.Begin(GL.QUADS)
jit.gl.texcoord( 0 , 0 ) gl.Vertex( -1 , -1 , 0 )
jit.gl.texcoord( 1 , 0 ) gl.Vertex( 1 , -1 , 0 )
jit.gl.texcoord( 1 , 1 ) gl.Vertex( 1 , 1 , 0 )
jit.gl.texcoord( 0 , 1 ) gl.Vertex( -1 , 1 , 0 )
gl.End()
jit.gl.unbindtexture( "tex2" , 1 )
jit.gl.unbindtexture( "tex1" , 0 )
end
jit.gl.bindtexture(texname, texunit)
-- draw some geometry
jit.gl.unbindtexture(texname, texunit)
jit.gl.bindtexture
and jit.gl.unbindtexture
are used to bind and unbind texture. The first argument is a texture name and the second argument is the texture unit to assign the texture to. Every graphics card has a fixed number of slots called texture units. The number of texture units a card has determines how many textures can be used simultaneously. This number is usually 8 but can be 16 or even 32. To see how many texture units your card has, go to the Options Menu > OpenGL Status. Under the OpenGL Limits > Textures item, you'll see MAX_TEXTURE_UNITS and a value next to it. This is the number of texture units your card supports.
jit.gl.begincapture(texname)
-- draw some geometry
jit.gl.endcapture(texname)
jit.gl.begincapture
and jit.gl.endcapture
are used to render drawing commands to a texture instead of to a window. The only argument is the texture name.
local gl = require( "opengl" )
local GL = gl
local pi = math.pi
local tex = jit.new( "jit.gl.texture" , this.drawto)
tex.dim = {1024, 1024}
function draw()
-- capture to texture
jit.gl.begincapture(tex.name)
gl.Color( 1 , 1 , 1 , 1 )
gl.Begin(GL.LINES)
for i= 0 , pi, pi/ 100 do
gl.Vertex(math.cos(i), math.sin(i* 2.4 ))
gl.Vertex(math.cos(i+pi), math.sin(i* 2.4 +pi))
end
gl.End()
-- end capturing to texture
jit.gl.endcapture(tex.name)
-- draw the result
gl.Color( 1 , 1 , 1 , 1 )
jit.gl.bindtexture(tex.name, 0 )
gl.Begin(GL.QUADS)
gl.TexCoord( 0 , 0 ) gl.Vertex( -1 , - 1 , 0 )
gl.TexCoord( 1 , 0 ) gl.Vertex( 1 , - 1 , 0 )
gl.TexCoord( 1 , 1 ) gl.Vertex( 1 , 1 , 0 )
gl.TexCoord( 0 , 1 ) gl.Vertex( -1 , 1 , 0 )
gl.End()
jit.gl.unbindtexture(tex.name, 0 )
end
-- arguments can be either a table or list of values
screenpos = jit.gl.worldtoscreen(worldpos)
-- arguments can be either a table or list of values
worldpos = jit.gl.screentoworld(screenpos)
jit.gl.worldtoscreen
converts world coordinates into screen coordinates. jit.gl.screentoworld
performs the inverse operation, converting screen coordinate into world coordinates. Both functions can take either a table of values or a list of x, y, z values. The z-coordinate in jit.gl.screentoworld
is typically a value in the range [0, 1] where 0 represents the near clipping plane and 1 the far clipping plane, which can be used to cast a ray into the OpenGL scene as the code below demonstrates:
function castray(x, y)
local raystart = jit.gl.screentoworld(x, y, 0 )
local rayend = jit.gl.screentoworld(x, y, 1 )
return raystart, rayend
end
jit.gl.draw_begin(jit_gl_object)
jit.gl.draw_end(jit_gl_object)
jit.gl.draw_begin
and jit.gl.draw_end
operate on jit.gl objects. All jit.gl objects share a set of common attributes know as ob3d (or object 3D) attributes. Whenever a jit.gl object draws itself, it sets up the OpenGL state to reflect the settings of its attributes such as depth_enable
and blend
among others. When an object is drawn, the following sequence of calls takes place:
draw_begin(ob3d)
draw(ob3d)
draw_end(ob3d)
Sometimes it's useful to have explicit control over this sequence of calls within a jit.gl.lua script and jit.gl.draw_begin
and jit.gl.draw_end
enable this kind of control. jit.gl.draw_begin
sets up OpenGL state based on the object's ob3d attributes while jit.gl.draw_end
reverses the process. These calls must be used in pairs, otherwise OpenGL errors may occur.
gshape = jit.new( "jit.gl.gridshape" , this.drawto)
gshape.automatic = 0
function draw()
-- equivalent to gshape:draw()
jit.gl.draw_begin(gshape)
gshape:drawraw()
jit.gl.draw_end(gshape)
end
Vector Math Functions
The vector math functions in jit.gl.lua are located in the vec
module and are organized into six categories. These categories are:- vec.vec2
-
vec.vec3
-
vec.vec4
-
vec.quat
-
vec.mat3
-
vec.mat4 For more detailed documentation on the
vec
module, see the jit.gl.lua Vector Math Overview.
OpenGL Bindings
Detailed documentation on each function in the OpenGL module can be found on the jit.gl.lua OpenGL Bindings page. There are also bindings for the OpenGL Utility (GLU) functions at jit.gl.lua OpenGL GLU Bindings. This section describes common usage and techniques.
The OpenGL bindings in jit.gl.lua enable direct access to OpenGL commands. Nearly all of the OpenGL commands are available through the bindings. Wherever possible the arguments to an OpenGL function in Lua match the arguments described in the OpenGL man page for that command. Since OpenGL is a C interface, type information and the number of arguments a command must take are fixed. Lua does not have this restriction, so in an effort to simplify the interface into OpenGL, functions that only vary based on type and the number of arguments have been collapsed into a single function. For example, the OpenGL command for specifiying a vertex of geometry as the following variations:
void glVertex2s(GLshort x, GLshort y);
void glVertex2i(GLint x, GLint y);
void glVertex2f(GLfloat x, GLfloat y);
void glVertex2d(GLdouble x, GLdouble y);
void glVertex3s(GLshort x, GLshort y, GLshort z);
void glVertex3i(GLint x, GLint y, GLint z);
void glVertex3f(GLfloat x, GLfloat y, GLfloat z);
void glVertex3d(GLdouble x, GLdouble y, GLdouble z);
void glVertex4s(GLshort x, GLshort y, GLshort z, GLshort w);
void glVertex4i(GLint x, GLint y, GLint z, GLint w);
void glVertex4f(GLfloat x, GLfloat y, GLfloat z, GLfloat w);
void glVertex4d(GLdouble x, GLdouble y, GLdouble z, GLdouble w);
In Lua, all of the variations are contained within a single function:
-- The same function, different number of arguments
gl.Vertex(x, y)
gl.Vertex(x, y, z)
gl.Vertex(x, y, z, w)
In OpenGL, enumerations play an important role for specifying different modes of behavior. An enumeration is simply a number with a particular meaning. For example, OpenGL has a lot of functionality that can be enabled and disabled such as depth testing, blending, etc. To enable or disable a particular bit functionality, an enumeration specifiying the functionality is passed to the glEnable or glDisable command. In C this looks like:
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
The Lua bindings for OpenGL also have enumerations. These are stored in the same location as all of the OpenGL functions, which is simply a giant table. Often times it is convenient to have the Lua code look as much like the C code as possible so that when copying example code or switching between C and Lua there is minimal cognitive overhead. We can do this by aliasing the OpenGL module table to the variable 'GL'. When jit.gl.lua loads a script, it autmatically makes available the opengl
table, which contains all of the OpenGL functions and enumerations. The following idiom allows us to write code that more closely resembles C-style OpenGL code:
-- set the OpenGL module to the variable 'gl'
local gl = require( "opengl" )
-- alias the OpenGL module to the variable 'GL' to emulate the C enumeration style
local GL = gl
function draw()
gl.Enable(GL.DEPTH_TEST)
gl.Disable(GL.BLEND)
-- draw some geometry
end
Color Functions
In addition to the OpenGL module, jit.gl.lua also has a color module built in. The color module contains funtions for translating betwee RGB space and Hue-Saturation-Luminance (HSL) space. It also contains a large number of pre-defined color values. The color values are given in RGB form. Here are some examples:
chocolate = { 0.823529 , 0.411765 , 0.117647 }
lightcoral = { 0.941176 , 0.501961 , 0.501961 }
slateblue = { 0.415686 , 0.352941 , 0.803922 }
There are 115 colors in total. For the full list, see the jit.gl.lua Color Bindings documentation.
The main functions in the color module are RGBtoHSL
and HSLtoRGB
. While colors in both spaces are defined by three values, an optional fourth value representing the alpha channel can also be passed in. The alpha channel is not involved in any of the calculations, but is simply passed through untouched.
hsl = RGBtoHSL(rgb)
hsl = RGBtoHSL(r, g, b)
hsla = RGBtoHSL(rgba)
hsla = RGBtoHSL(r, g, b, a)
rgb = HSLtoRGB(hsl)
rgb = HSLtoRGB(h, s, l)
rgba = HSLtoRGB(hsla)
rgba = HSLtoRGB(h, s, l, a)
For example, the code below converts an RGBA color to an HSLA color, lightens it, and then gets back the result into RGBA space.
color = { 1 , 0.2 , 0.2 , 0.5 }
hsla = RGBtoHSL(color)
-- lighten the color
hsla[ 3 ] = hsla[ 3 ]* 1.1
color = HSLtoRGB(hsla)
The other three functions in the color module are for manipulating HSL colors. They are designed so that the color manipulation functions can be chained without having to assign any results to a variable. These functions are hue
, saturate
, and lighten
.
res = hue(hsla, hue_offset)
res = saturate(hsla, saturation_scale)
res = lighten(hsla, luminance_scale)
hue
offsets the hue of an HSLA color by a given amount. saturate
and lighten
scale the saturation and luminance components of an HSLA color respective.
-- lighten the color
color = HSLtoRGB(lighten(RGBtoHSL{ 1 , 0.2 , 0.2 , 0.5 }, 1.1 ))