The term `Intensionality' is taken from formal logic. It comes from `in tense', meaning: relating to time. The intensional meaning of a term is not some static object, but rather the object as it varies in time. This notion is extended to mean `varying in some context' instead of just time. In IL these contexts are: 1, 2 and 3 dimensional space:
These spaces, or domains, (see section Map domains) can exist in two forms, discrete or continuous. A discrete domain is conceptually linked with a sampled data set for instance a sound file or a digitized image. On a continuous domain any (mathematical) function can be defined. Switching from one kind of domain to the other is done by interpolation or sampling. IL is an intensional language in the sense that any data-type can transparently take the form of a function on one or more of the above mentioned domains. In any expression involving terms of a certain type, the value of that term can be dependent on these domains. So by just defining a pixel-type and operators for it, automatically these operators work on complete computer animations, being functions on the 1-D and 2-D domains. By this mechanism unification is established of (at least) texture mapping, image processing, procedural animation and sound synthesis.
ILis a pragmatic, interpreted functional language. Pragmatic in the sense that it takes the consequences of implementation issues to the limit. Thus, being interpreted and functional means that functions and strings are the same thing. In other words, quoting and function calling are complementary operations.
Basic control structures are (recursive) function calls and conditional expressions.
The language implementation software is written in C and consists of a kernel and any number of user modules. These modules implement the data-types, with functions and operators working on these. The kernel and a number of modules can be supplied as a library, because adding new types can be done without recompiling the kernel, just by linking the desired modules with the kernel. Adding new types is done in a true object-oriented fashion, with possible overloading of function names and operator symbols. The basic interpreter cycle can be mixed in a generic way with any interface mechanism, such as motif's dispatch event loop, to establish graphical user interfaces and multi-media operability.
The simplest way to begin with the language is just calling the executable from the operating system. IL will respond with a prompt looking like:
The interpreter will from now on do nothing else but reading a line from the terminal, interpreting it as an expression, evaluating it and printing the result (if not coming across an error). After this it will repeat the cycle by prompting again. Only a few types will necessarily always be built in. In practice there will probably be a lot of them. Anyway, you can be sure there will be integers. So, the following dialog is a possible one:
} 3+7 10 }
More then one expression can be put on one line, delimiting them by semicolons. They will all be evaluated in order, but only the last result will be printed.
} 3+7; 5*6+7*2 44 }
It is important to note that expressions are really the only element of IL. So anything that happens will always leave a result. Two other types that will always be there, are floating-point reals and booleans. All the functions and operators you expect for them are (hopefully) implemented, especially the normal (C) form of conditional expression is provided.
} degree(T); sin( 30 ) < 25e-2 ? 7+8 : 3^5 243.0 }
is a build in function to set the mode of operation for trigonometric functions. There are several of these mode-setting functions, and they all are called in the same way. They can be called without a parameter to just deliver the current setting of the mode, or they can be called with a new setting (and delivering it as a result value). Another one of these is
eolmode. In the default case this mode is on. The end of line is then interpreted as end of expression, i.e. a semicolon is implied. With this mode off, semicolons at the end of any expression are obligatory and expressions can be extended across the end of line.
Fare built-in identifiers for the two boolean values. When degree mode is switched off (by calling
degree(F), the default) all trigonometric functions work in radians.
In the above example
sin is called with an integer value, yet it is only defined for real-valued parameters (and complex ones). This is an example of coercion and it plays an important role in the language. More about that later.
It is also clear from the example that the two alternative parts of a conditional expression need not be of the same type (but be careful when assigning the result of an expression to an identifier, as explained in the next section). The condition can, apart from boolean, also be of the integer or real type, with the usual interpretation of 0 being false and anything else being true. The empty list also functions as false in this context. The alternative part of the expression may be omitted, implementing something like a conditional statement without `else' part. This construct still is an expression though, returning an empty list in case the condition evaluates to false.
One important built in function is exit. When called it does what you expect it to do. But make sure you call it, rather then ask what it is:
} exit implicit function } exit() %
exit means you evaluate an expression of the type
implicit function, of which the value can not be printed, so you only get an indication of its type. Calling this function gives the desired effect.
In the usual way it is possible to give names to results of expressions for later use. These names, or identifiers, are built from letters and digits, the first character being a letter (note that other characters, fi. `_', are not allowed in identifiers, so they are available in operator symbols). The difference between upper and lower case letters is significant and only the first 128 characters are taken into account. Identifiers are not declared. They come into existence when first assigned to and cannot change type afterwards.
Note that assignments are also expressions with as their result the value of their right hand side. This can be significant when coercion is needed to assign to an existing identifier.
} i = 7 7 } i = exp( 1 ) 2.7183 } i 2 }
i is initialized with an integer value, so it will be an integer identifier through its existence. When a real value is assigned to it, this value will be coerced to an integer value by truncating it. The result of the assignment expression will however be the unaltered real value.
Above we noted that implicit functions are expressions by themselves, with their own types. This has as a consequence that they too can be assigned.
} quit = exit implicit function } quit() %
Apart from the built in implicit functions, the IL programmer has the possibility to write her own. The type for this feature is the string type. Just like booleans and integers, the string type is always implemented. Putting a row of characters between double quotes notates its values.
} "this is a string" this is a string }
Quotes within strings are written by doubling them:
} "this is a string quote: """ this is a string quote: " }
Strings can be interpreted as functions by calling them as usual. That is, by following a string valued expression by parentheses. This means that the control for the interpreter is switched from the terminal input to the function string. This mechanism is of course the same as in most job-control (shell) languages. Here it is consequently exploited in the expression evaluating interpreter paradigm, resulting in a functional kind of language.
} f = "/exp(1)" /exp(1) } f() 0.36788 }
(Note that the slash symbol
/ functions as a unary operator meaning 1 divided by ..., in accordance with the usual interpretation of the unary minus operator)
These functions can be called with parameters. The parameter evaluation mechanism is always `call by value'. The value of a parameter is obtainable inside the function with one of the operators
$. They work in exactly the same way. In the following
% will be used but can be replaced with
n being an integer, will evaluate to the nth parameter.
} f = "%1 + %2" %1 + %2 } f( 2, 6 ) 8 }
is a genuine unary operator, expecting an integer valued expression, so it is not necessary to use it with a plain integer. Moreover, there is the implicit function
pars, supplying the current number of parameters the function is called with.
} f = "%pars()" %pars() } f( 6, 7, 8 ) 8 } f(1,2,3,4,5,sinh(2)) 3.6269 }
There are some tricks to pass arguments from inside a function to a function that is called in a nested fashion. This is best explained with a symbolic example. Suppose
f is called with arguments as:
f( a, b, c, d, e );
g is called passing the same arguments:
g( %1, %2, %3, %4, %5 );
instead of this it is possible to write:
The difference of these two possibilities comes to light when calling with new arguments:
g( x, y ];
is equivalent to
g( x, y, %1, %2, %3, %4, %5 );
--- parameters are added at the start of the argument list.
g[ x, y ];
is equivalent to
g( x, y, %3, %4, %5 );
--- parameters at the start of the list are overwritten.
There is also a possibility to add parameters at the end of the argument-list:
g[ x, y );
is equivalent to
g( %1, %2, %3, %4, %5, x, y );
but this construct has the added functionality that it forces a tail-recursive call. (See section Recursion) It will not return! This is therefore not legal as term in an expression.
There is the notion of local identifiers in the language. Identifiers can be introduced in a function by assigning to them. They will not conflict with global identifiers. Global identifiers can be referred to however, when they are not used as local identifiers. This goes for local identifiers on a higher level too, when function calls occur in a nested way. When identifiers are evaluated, they will be looked for in succeeding lower levels, until the global level. In the following example the implicit function
} degree(F); pi = 4*atan(1); 3.1416 } f = "print(pi); a = 6; g(); a" print(pi); a = 6; g(); a } g = "pi = F; print( pi ); a = exp(1); print( a )" pi = F; print( pi ); a = exp(1); print( a ) } f() 3.1416 F 2.7183 6 } pi 3.1416 }
There is the possibility to assign explicitly to a global identifier from within any function by preceding the identifier with
} g = "::pi = 180" ::pi = 180 } g() 180 } pi 180.0
Another way to get this effect is by using the implicit function
assign. This function takes two parameters, the first being a string. This string is interpreted as a global identifier and the value of the second parameter is assigned to it.
} assign( "blob", 666 );
works the same as
assign, but protects the identifier (as assignment with :=).
The use of
define is primarily intended for the creation of user defined operators, because the symbol string has not to fulfil the demands required by the identifier syntax. The following example demonstrates this feature.
} fac = "n = %1; n ? n*fac(n-1) : 1" n = %1; n ? n*fac(n-1) : 1 } assign( "~", operator( fac ) ) prio 0 n = %1; n ? n*fac(n-1) : 1 } ~6 720 }
The implicit function
operator creates an item of the type
Operator. The parameter is the user-defined function associated with the operator. There is an optional second parameter denoting the priority of the operator, useful in the case of binary operators. It is possible to overload implicit operator symbols (like
+) in this way. In this case it is not possible to overwrite the built-in priority. The interpreter will use the user-defined operator if the implicit operator is not applicable in the type context.
In the following example a pseudo array mechanism is implemented. The printing of results is controlled by the echomode.
# is string-concatenation. It is overloaded to create identifiers with an index.
getid is the complementary function of
assign. Also a simple
for statement is implemented.
} echo(F) } assign( "#", operator( "%1 # sprintf( ""%4.4d"", %2 )" ) ) } print( "abc"#5 ) abc0005 } arrayass = "assign( %1#%2, %3 )" } arrayval = "getid( %1#%2 )" } for = "%1 > %2 ? return() : 0; }" %3(%1); for( %1+1, %2, %3 )" } rootable = "rootable" } rootass = "assign( rootable # %1, sqrt( %1 ) )" } for( 1,10, rootass ) } print( arrayval(rootable,5) ) 2.2361 }
Note the change of the prompt to
}" when entering multi-line strings. The
sprintf function is a partial copy of the C function. An alternative to the
getid use here, because it uses correct identifier syntax, would be:
} arrayval = "(%1#%2)()"
The use of the function
return should be obvious. In this case it makes the function to return the integer value 0. An alternative for this can be given as a parameter to return.
We already encountered some uses of recursion in the definition of
for. A better
for would be:
} for = "%1 > %2 ? pars() >= 4 ? %4 : 0 : for( %1+1, %2, %3, %3(%1) )"
} cfor = %1 < %2 ? cfor( %1+1, %2, %3, %3(%1) ) : pars() > 3 ? %4 : 0"
returning the value of the last iteration.
This kind of recursion is so-called tail-recursion. The interpreter knows the recursive call is the last one, so does not stack any items unnecessarily. The number of iterations will not be limited by stack-overflow. One has to be careful however when one wants local identifiers to be known to higher levels. In the case of tail-recursion local identifiers will be removed. So sometimes tail-recursion has to be deliberately prevented.
} f = "n=%1; g()" } g = "n*(n-1)" } print( f( 6 ) ) error 127: identifier unknown n } f = "n=%1; m=g(); m" } print( f( 6 ) ) 30 }
An identifier can be protected from assigning to it (making it effectively a constant) by calling
} pi = arg( -1 ); e = exp( 1 ); } protect( "pi", "e" ); } pi = 666; error 275: assignment to protected variable pi= }
The effect can be canceled with
unprotect. Protection can also be obtained with the definition symbol
} e := exp(1);
is equivalent to
} e = exp(1); protect( "e" );
The existence of a global identifier can be tested with
} echo( T ); exists( "dfi" ) F } dfi = 469; exists( "dfi" ) T }
Lines beginning with /* are considered comment (outside strings).
} /* schuitje varen, theetje drinken }
Instead of typing expressions at the terminal it must of course be possible to write programs in text files. The interpreter can be made to read such a file by calling the function
read with the filename as parameter.
} read( "flofbla" )
The file extension .il is added to the filename and the file is sought for, first in the current directory, then in the directory designated by environment or option. The read call will properly return and can be nested. Executing the function
exitfile can terminate reading a file. When starting IL, a number of filenames can be given as parameters. These will be read before starting an interactive session.
There is a module concept in IL. Such a module is a piece of IL code between calls of
endmodule. Global identifiers in such a module will only be known inside that module, except when they are explicitly exported. Likewise external identifiers can be imported. Modules are internally identified by an integer. In the default case
startmodule will take the next available integer to create a new module. It is however possible to give an explicit number as parameter. In this way it is possible to `reopen' an already ended module. Both functions return the number of the module.
} modnr = startmodule(); } init = "::sum=0"; } add = "::sum=sum+%1"; } export( "init", "add" ); } endmodule(); } init(); } add( 7 ); add( 8 ); } sum error 127: identifier unknown sum } startmodule( modnr ) } getsum = "sum" } export( "getsum" ) } endmodule() } print( getsum() ) 15 }
There are some things all types have in common. They have a name, an integer identification, a size and a number of methods applicable to them.
types prints a list of all implemented
types. When creating a new type in a C-module, the name is given to the kernel. Let that name be "Mine". The kernel then creates two IL identifiers.
Minetyp is an integer identifier with as value the internal identification.
Mine is an implicit function, which converts any parameter to the type
Mine if at all possible.
} Inttyp 5 } Int( pi ) 3 }
The integer type identification is very useful in combination with
typeof, a function that returns this integer for any parameter
} typeof( "abc" ) == Stringtyp T }
The name of a type can be obtained with
} typename( 6 ) Real }
One of the methods that are defined when a new type is called into existence is the one that is invoked by a call of
&. For some dynamic types there are two different kinds of copy. The so called deep-copy, with operator
&&, recursively copies sub elements of a value. We will see a use for this feature with lists. (See section List)
} l = [1,2,3); list } m = l list } m == l T } m = &l list } m == l F }
Conversions from one type to another are done by methods called coercions. A type together with its coercions can be regarded as a class. Which method is invoked for an implicit function or operator is determined by the type context. So all parameters have their influence on the choice of method instead of one parameter determining the class. If there is no applicable method for the current context, the kernel tries to create one by coercing the parameters. In this way inheritance is established. A simple example is adding two numbers. There is an adding method for two integers and one for two reals. When adding an integer and a real, the integer is converted to a real and the real adding method is applied. With each coercion a scheme-number is associated. Likewise each function or operator belongs to such a scheme. Only matching coercions are sought for when looking for inheritance. The default scheme is 0. In the list printed by
types, for each type one sees a line with:
For all types the operators
!= are defined, returning a boolean value. The normal boolean operators are defined as:
There is a generic list-type build in the language. The standard means to implement this type is the constant
nil representing the empty list and the functions
tail with their usual interpretation. All types can be mixed as list elements.
} l = cons( T, cons( 2, cons( 3.0, nil ) ) );
there is the following construct:
} l = [ T, 2, 3.0 );
is an index operator
} l'1 2 }
l'n is equivalent to applying
tail followed by
head. The same, but without the final
head, is accomplished with
} l@1 list }
The value of a list is not printed, just the type indication. One can define a function
lprint with the following program fragment:
startmodule(); prlelt = "print( lst'%1 )"; import( "cfor" ); lprint = "lst = %1; cfor( 0, length( lst ), prlelt ); lst"; export( "lprint" ); endmodule();
The implicit function
length gives the number of elements of a list. With
lput the head of a list can be replaced with any other item. In combination with
@, any element can be replaced.
} lput( l@1, 5.0 ); } lprint( l ); T 5.0 3.0 }
Lists are dynamic. After an assignment
} m = l; } lput( m, F )
both l and m are the same changed list.
Here the difference of simple copy
& and deep copy
&& is significant. A simple copy is a new list with references to the same elements as the old list (for dynamic elements, for instance lists). A deep copy gives you recursively deep-copied elements.
There is a concatenation operator
There are complementary operators for removing and inserting sublists:
} l > n
remove a sublist of length
l, starting with
l'1. The result of the expression is the removed sublist.
} l < lx
lx in list
These types are
Quat. There are standard coercions in the direction:
and under scheme 1 the other way.
Realhave their standard notations for constants build in the scanner of the kernel. For
Quatthere exist functions to define new values by `summing up' the real constituents.
The usual notations for decimal, octal and hexadecimal integers are available.
} 5 5 } 0777 511 } 0x4d 77
All the relational operators are defined:
> < >= <=
as well as the arithmetical
+ - * / %
and logical (bitwise) operators
! & | |^
abs is defined for integers.
The binary operators
give the maximum and minimum of two integers.
s are coercible to
Ints under scheme 1. So this coercion hardly ever occurs automatically, but it does when calling
Int. The same goes for complex values and quaternions.
The same relational and arithmetical operators are defined as for
% implements the `drem' function). On top of these there are the power operator
^ and unary
/. The following functions are defined for both real and complex values:
abs, sqr, sqrt, exp, log, sin, cos, tan, asin, acos, atan,
sinh, cosh, tanh, asinh, acosh, atanh, sec, cosec, asec, acosec,
sech, cosech, asech, acosech.
They will return complex values for real arguments whenever necessary.
These are some more functions for reals:
sinc, vsin, vcos, hypot, atan2, vatan2, J.
sinc : i
sin(pi*x) / (pi*x)
The trigonometric functions starting with a
v are normalized (with regard to their domain as well as to their range) to the interval [0,1], for ease of use with (2-D) texture maps.
J is equivalent to the C bessel-function
hypot works for two or three arguments.
Complex values can be defined from a pair of reals with
pcmplx, the latter for representation in polar form. The following functions return the obvious real constituents:
re, im, abs, arg.
There is the special complex function
conj for the complex conjugate. A special use of complex values is their interpretation as range type for texture bump maps.
Quaternions are arithmetically fully implemented. They can be defined with the function
quat with either 4
Reals as arguments, or 1
Real and 1
Real3. They are implemented with a 3-D imaginary part. There is a normalization function
norm to bring them on the `unit sphere' for the standard interpretation as rotations. There are coercions from and to transformation matrices. The following operators and functions are defined:
- / (unary)
+ - * / ^
abs, re, im, conj, sqr, exp, log
Already a lot has been said about strings. There are some functions and operators worth mentioning:
The basic coercions
String are overloaded to handle ASCII-character codes.
As base classes for the geometrical types and the intensional domains the following types are defined: (with their `C like' equivalent)
They all have basic definition functions with the same names but starting with lower case letters:
int2, int3, real2, real3, real4, intl2, intl3, reall2, reall3.
They all have the index operator
} x3 = real3( 1, 2, 3 ) 1.0 2.0 3.0 } x3'2 3.0 }
The single indexed types all have:
The rest only have unary
There are the following scheme 0 coercions:
and all of these reversed in scheme 1.
Real2 is taken to represent 2-D points. The geometrical type of 2-D lines is
Line2. Their internal representation can be:
There are coercions to and from
Real3. There is the type
Transform2, which is a 3*3 matrix, representing 2-D transformations.
Reals or 6
Reals (homogeneous) or 3
Real2s to make a
Line2(line) and a
Realto return a 2-D point on the line.
linepnt( l, 0.0 )is equivalent to
linepnt( l, 1.0 )is equivalent to
l'0 + l'1
hypot( r2 )
hypot( r2'0, r2'1 )
Reals or 1
Real2to make a translation matrix
Realand either a point or a line to make a scaling matrix
Realand a point to make a rotation matrix (takes degree mode into account)
These are the operators:
Line2and returns the transformed item, or multiplies two matrices.
Here a plane type
Plane3 is introduced. It is based on
Real4. Also the type
Transform is introduced (plain
Transform is 3-D). All the 2-D functions have their 3-D counterpart with the following notes.
Transformto the power of a real. Of special interest to perform `inbetweening' on the transformation level. It is a complex algorithm that does not try to take a transformation apart in possible constituents but instead performs pure matrix algebra.
As mentioned before, special care has been taken to be able to mix matrices and quaternions by defining the appropriate coercions.
scheme 1: both ways
matrix multiplication resides under scheme 1, also because of some technicalities involved with the above mentioned power algorithm.
Besides as geometrical coordinates, Real3 can also be interpreted as representing colors. In this respect there are a couple of functions:
The first two are obvious, except that they incorporate a correction to avoid the usual first order discontinuities in hue transitions. The third one interprets its argument to lie in a normalized YUV space. Normalized in the sense that each value in the interval ( [0,1], [-1,1], [-1,1] ) represents a color.
The pixel type is based on
Real4, interpreted as consisting of red, green, blue and `alpha'. Coercions to -- and from
Real3 (colors) and
Real exist. The following operators implement the usual compositing algebra:
- pix1 +~ pix2
pix1 -~ pix2
pix1 <- pix2
pix1 /~ a2
pix1 %~ a2
d <~ pix
d >~ pix
Map is the type of intensionally defined entities. They are functions of one or more of the standard domains to any one of the static IL types. They are normally not evaluated in the interpreter context, but when invoked in some kind of rendering operation, like ray-tracing, image- processing or sound-synthesis. An exception to this is the function
evalmap, which explicitly evaluates a map after setting the domain with
All functions and operators for a certain type are automatically applicable to maps of that type, delivering maps as result. So the syntax for creating maps is exactly the same as for normal expressions. Only when one of the arguments is a map, the result is a map, which is a compiled version of the expression instead of the evaluated result.
The type a map evaluates to is obtainable with
There are 3 continuous map domains, one for each of the relevant dimensionalities. They are available through implicit functions:
Some possible interpretations are:
The following is an example that will produce a chessboard pattern when used as a texture map:
d = m2(); chs = Int( 8*d'0 ) + Int( 8*d'1 ) & 1 ? 0.0 : 1.0;
chs is directly usable as intensity parameter for an attribute in a solid model. In fact, using scalar multiplication of
Real2 and because integers are converted to real when used as intensity, it is also possible to write:
d = 8*m2(); chs = Int( d'0 ) + Int( d'1 ) & 1;
As 1-dimensional domains are normally used as time-representation, there are 3 discrete versions of them, reflecting the different uses of `sample-time'. These are:
And for the 2- and 3-dimensional cases:
Most of the time, when mixing discrete and continuous domains, the system does what you expect it to do, namely, converting to and from the relevant domains. To this end there are some quantities, which exists globally as well as associated with individual maps. These are (and can be set or get with):
: bounds of
m2din relation to
: bounds of
m3din relation to
m1dain relation to
m1, default 25
: audio control rate,
m1dcin relation to
m1, default 100
: audio sample rate,
m1dsin relation to
m1, default 44100
So when, for instance, you multiply a (discrete) image with a (continuous) noise, the noise function is automatically sampled (evaluated) at points corresponding with the image pixels, converting the image space to the square unit interval.
Apart from this there is a complete generic way to switch domains with
trfmap( m, d )
The result is the map
m, but with
d as new domain, where
d can be a map itself. The following example is a function to convert a 2-D map to a 3-D solid map with a `ball-projection':
ballprj = "m = m3(); x = m'0; y = m'1; z = m'2; u = vatan2( y, x ); r = hypot( y, x ); v = 2 * vatan2( r, -z ); trfmap( %1, real2( u, v ) )";
is supposed to be a 2-D map, dependent on
m2(). So, the second parameter of
trfmapis of type
Real2. This expression is the new domain. It is solely dependent on
m3, so will be the result map.
When maps on discrete domains are evaluated on continuous domains (perhaps as a automatic step in a conversion to another discrete domain), the values are to be interpolated. The way this is done can be set with
For the linear case the function
linpol must be implemented for the relevant types as a function that interpolates between two values of that type according to a third real argument in [0,1]:
} linpol( 4.2, 5.7, 0.4 ) 4.8 }
It is implemented for:
Real, Real2, Real3, Pixel, Complex, Quat, Transform
Already mentioned was the function
evalmap. It evaluates a map. There are global domain values to form a context for this evaluation. These can be set with
setmapdom. Which domain is meant, is determined by the type of the argument, on the understanding that an integer is taken to relate to
m1ds, setting the other 1-D discrete domains according to the relevant frequencies.
} setmapdom( real3( 5.0, 6.7, 88.9 ) ); 5.0 6.7 88.9 } evalmap( 6*m3() ); 30.0 40.2 533.4 } setmapdom( 10000 ); 5 } evalmap( m1dc() ); 22 }
Maps can be regarded as trees, with maps as nodes and constants as leafs. The number of branches at a node is the result of the function
pars. The nth branch of a map can be obtained with the
@ operator. The branch can be changed with the
setmap function. The general cast-function Map makes a constant valued map of any value.
} m = m2()'0 + 3; map } pars( m ) 2 } pars( m@0 ) 2 } m@1 3.0 } setmap( m, 5.0, 1 ) map } m@1 5.0 } setmap( m, m2()'1, 1 ) map } m@1 map } m@1@1 1 } m = Map( 6 ); map } /* default index of setmap == 0 } setmap( m, 8 ) map } evalmap( m ) 8
To check for equivalence of the top-node of maps the function
islikemap is available.
} islikemap( 3*m2(), m1()*dnoise(m2()) ) T /* both are multiplication of scalar and 2-D vector
The domain dependency of a map can be obtained as a bit pattern with
idep. The different dependency values are built-in constants:
} dep1c 1 } dep2c 2 } dep3c 4 } dep1da 256 } dep1dc 512 } dep1ds 1024 } dep2d 2048 } dep3d 4096 } idep( m1ds() * m2() ) 1026 }
The implemented random functions are based on the `rand48' package from the standard C library. The seed can be set (or obtained) with
startrand in the following ways:
} startrand() 123 4567 890 } startrand( int3( 654, 765, 56766 ) ) 654 765 56766 } startrand( 654, 9966, 12549 ) 654 9966 12549 } startrand( 555555555 ) 13070 6883 8477 } /* 13070 is a default value in the last case }
The following random functions are implemented:
random( b )
random( l, u )
frandom( x )
frandom( x, y )
normrand( a )
normrand( s, a )
There are the usual process times available with
systime. They give their result in seconds as a real. Just
time is a real-time function, default starting with startup time, but a time point can be set with
} usrtime() 0.17186 } systime() 0.07421 } time() 149.67 } settime( 1234 ) 1234.0 } time() 1240.8
The well-known fractal landscape `midpoint displacement' algorithm is implemented to create 2-D maps. The size of the defining grid can be set with
fracsize. Its argument is either an integer, defining the size of a square, or an
Int2, defining a rectangle. In either case the size is rounded to powers of 2.
} fracsize(100 ) 128 128 } fracsize( int2( 300, 900 ) ) 256 1024 }
The fracsize becomes the
dbnd2 of the resulting map, which is
m2d dependent. The maptype can be either real or complex (bumpmap!).
} rfract() map } bfract() map }
A parameter h can be given, defining the `fractal roughness' as usual. The default is 0.75. The normal range is in [0,1]. The resulting fractal dimension is 3-h. This parameter may be a map!!
The famous `Perlin noise' is implemented for 1, 2 and 3 dimensions. They really are normal functions, creating maps when called with the relevant domain. An optional second real parameter is an overall scaling factor. (For ease of use. The same effect could be obtained by multiplying the domain.) An optional third parameter is like an integer seed for the creation of independent noise functions. For each dimension there are three noises:
So, normal 3-D solid noise is obtained with:
} ns = noise( m3() );
and a higher frequency 2-D bump noise with:
} bns2 = cmplx( bnoise( m2(), 0.1, 0 ), bnoise( m2(), 0.1, 1 ) );
and a 3-D vector valued bump map:
} vns = dnoise( m3() );
For all dimensions it is possible to generate a cloud of (particle) coordinates according to a real valued map, interpreted as a density function. A simulated poisson process generates the coordinates. The function result is a list of values in the particular domain, which is determined by the type of the limiting interval and the dependency of the density map.
} pl = density( 100*noise( m3() ), real3(0,0,0) |+ real3(1,1,1) ) list } length( pl ) 40 } pl'0 0.06661 0.39748 0.32393 } pl = density( 100*noise( m2() ), real2(0,0) |+ real2(1,1) ) list } length( pl ) 53 } pl'0 0.06812 0.20598 } pl = density( 100*noise( m1() ), real2( 0, 1 ) ) list } length( pl ) 66 } pl'0 0.00253 }
Real valued maps can be dithered. This dithering is meant for `artistic' effects, it is not to be confused with means for exploiting limited display resources. The method is stochastic dithering, with a parameterized number of levels (default 2).
} m = dither( rfract() ); } n = dither( rfract(), 5 ); } s = dither( rfract(), 2+6*noise(m2(),0.1) );
ode solves an ordinary differential equation with initial value. For this purpose a new primitive map is available;
v3 is a function giving the velocity vector as a 3-D domain. With
m1(), functions can be defined for velocity and/or acceleration.
} mp = ode( ma, x0, v0 );
} mp = ode( ma, mv, x0, v0 );
} mp = ode( mv, x0 );
the map function for acceleration,
mvthe map function for velocity,
v0the initial values for place and velocity. Both calls can have a parameter extra for the order of the integrating algorithm (0, 1 or 2 giving first, second or fourth order). The default is 1.
} mp = ode( ma, mv, x0, v0, 2 );
The result is a
m1da dependent map giving the place solution of the equation. With
velocity the derivative can be obtained;
} m = velocity( mp );
With the function
order the integration order can be inspected or changed.
} order( mp ) 1 } order( mp, 0 ) 0 }
Solid models are trees with boolean operations at the nodes and primitives as leafs. Nodes as well as leafs can have lists of attributes attached to them. In IL they are implemented as a new type:
Different primitives are distinguished by an identification integer. The integers for the implemented primitives are built-in constants:
sbal, scyl, scon, scub, stor, spol, siso.
A new primitive is created with a call of
prim, with the identification as parameter.
} s = prim( sbal ); bal }
Some primitives, like a torus, need more information to be created. In IL this is arranged with attributes.
These are the above mentioned boolean operators to create a new Solid from two others:
+ operator can be applied as a unary operator to create a single extended node without operation.
For the selection of one of the two constituents of a constructed solid the index operator
' is implemented with a boolean selector.
Solids can be multiplied with transformations. This can happen on both sides, which is reflected in the order of matrix multiplication. Of course the transformations can be (time depended) maps. Transformations can be applied on each level of nodes and leafs to create a hierarchical transformed (animated) model. As solids constitute a dynamic type, the transformations are accumulated `inside', so no new assignments are necessary.
} translate( 1, 2, 3 ) * s } s * rotate( 30, point(0,0,0) |+ point(1,0,0) ) } scale( 10*mt(), point(0,1,0) ) * s
A transformation term in a solid can also be
m3 dependent. It is evaluated in the context of the hierarchical model and the `place' of the term in the current transformation.
The transformation of a solid can be obtained with the function
A single attribute is implemented as a list. The first element must be an identifying integer, the following elements are parameters with types according to that identification. An attribute to be `hooked' on a solid is a list of these lists.
Each solid has a number of `attribute hooks', again identified by integers. In addition to the general hooks, a leaf has some more to be able to give distinct sides their own attribute lists. A general solid can be given an attribute list with
} putnatr( s, i, al )
s the solid,
i the `hook' and
al the attribute list. Possible hooks are:
The attribute list of a solid can be obtained with
} al = getnatr( s, i );
In the same manner the attachment of leaf attributes is implemented with
getlatr. Here the `hook' is identified with the number of the side, starting with 0. A special hook is identified with
leafat. On this place it is possible to give the special primitive parameters that were mentioned above. For instance to give a torus a radius:
} st = prim( stor ); putlatr( st, leafat, [0.1) )
putlatrreturn the solid. The identification of the recognizable attributes with their parameters:
Real3: ambient light (only for this diffuse attribute)
Real: specular index
: reflection factor
: surface-normal modulation
Complex or Real3
special transparent solid properties (combine with refract and glass) (-->
: refraction index
: transparent color
light sources (-->
Point light sources can be classified as constant, linear or quadratic. This concerns the way the distance to the light source affects the resulting intensity. They also can be shadow casting. They all have as parameters their intensity at unit distance, their color and their place.
lspoint2 == lspoint
lspointsh2 == lspointsh
Real3: place cordinates
Other light sources are:
: angle of light cone in radians
Real: focus index
An lspot attribute is not to be used directly as a light attribute, but rather as a parameter for a point light source instead of the place.
All attribute parameters can be maps on any domain.
To be able to define polygonal primitives, a value of type
Poly can be attached to the
leafat hook of a primitive identified with
spol. An instance of such a type is created with
polymesh. It has two integer arguments, stating the number of vertices and the number of triangles. Further more there are two optional boolean arguments (default F), stating whether there are normals for the vertices to be supplied and whether 2-D (uv) coordinates will be defined for the vertices, to be able to control the definition of 2-D texture maps.
The number of vertices and triangles of a already `declared' polymesh can be obtained with
poltrianglecnt. To define the different values there are the following functions:
} polvertex( pm, vi, r3 ) } /* pm the mesh, vi vertex index, r3 the Real3 value } poltriangle( pm, ti, i3 ) } /* define the 3 vertex indices of the triangle with index ti } polnormal( pm, vi, r3 ) } /* define the normal direction of vertex vi } poluv( pm, vi, r2 ) } /* define uv-coordinate at vertex vi } polsolid( pm, b ) } /* b boolean to make the mesh a closed solid
All the above functions can be called without the last argument to obtain the current value. To make a primimitiv of a
} s = putlatr( prim( spol ), leafat, [pm) );
As Poly is a dynamic type, more primitives can be made of the same Poly.
A model created along the above-mentioned lines can be rendered by the built-in ray-tracer. In IL
raytrace is a function that returns a
m2d dependent map of the (real4) pixel-type. Apart from the model, a number of entities have to be defined, together constituting the so-called ray-trace environment. This environment is given as a second (optional) argument to the ray-tracer in the form of a list. Like attribute lists, this environment is a list of lists, each of these consisting of an integer identification and a value. These are the identifications, their types and default value:
Dither | Matting | Tapedump
Tapedump: creates a file of the resulting image.
Matting: creates a matte in the alpha-channel of the result
Dither: dithers the pixel-values instead of just rounding the calculated real pixel values. (Has nothing to do with IL map dithering)
Jitter: distributes the generated rays in their designated pixel-area as an aid in anti-aliasing.
Dangle: try to eliminate ambiguities of boolean operations when different surfaces coincide.
: grey (.5*white)
: unit matrix
curdepthfor reflection and refraction.
: none (-->unique filename generated)
(map): none define a background map instead of
The default value of each of the environment items is always obtainable with
Besides the call:
} mp = raytrace( s, envl )
} mp = anitrace( s, frfr, tofr, envl )
which renders an animation when
s is somehow time-dependent. Of course, in addition to
mp will also be
Ints, indicating the first and last frame.
Ray tracing is a form of rendering. In IL it is possible to give a reasonable formal definition of rendering:
extensionalizing a value on a discrete domain.
I.e. calculating data that constitutes, in `tabular' form, a map on a discrete domain.
To do this in general for an image there is the function
} md = writeimage( fname, m )
is a map, probably dependent on
m2dof the pixel type (
Real4, or coercible to it.
fnameis a string, indicating the filename for the image. With an optional third argument the number of `colors' (default 3) can be set to 4, keeping or creating the alpha channel. There is the animated version for time dependent maps:
} md = writeanimge( fname, m, frfr, tofr )
In addition to the optional fifth argument as above, there is the possibility to give a integer skip parameter n, rendering every nth frame.
To get existing images, the following are implemented:
} md = openimage( fname ) } md1 = openvalimage( fname ) } md2 = openbmpimage( fname ) } ma = openanimage( fname )
(openanimage tries to read frame 0. It is possible to give an alternative frame number as second argument).
x = openimage("portrait"); /* take existing image, set global pixelsize according dbnd2( dbnd2( x ) ); y = openvalimage("portrait"); z = openbmpimage("portrait"); /* define a color transforming function by rotating in rgb space pi2 = 2*arg(-1); collin = norm( real3(0,0,0)|+real3(1,1,1) ); colrot = "rotate( pi2*%1, collin ) * %2"; /* create a fractal defined color map fracsize(512); frcol = real3( rfract(), rfract(), rfract() ); /* define a distortion function, taking complex values as argument distm = "m2() + %1 * z"; disto = "trfmap( %1, distm(%2) )"; /* make a new picture dis = (0.5+0.5*y)*disto(1-hypot(z),0.1)*colrot( disto( y, 0.1 ), frcol ); writeimage( "distport", dis );
There is an optimized function to combine a list of linear deformed images in one image.
} md = rimage( fname, iml );
is a list of lists, each of which is composed of a 2-D transformation and an image generating map, as above in
writeimage. The animated version is:
} md = ranimge( fname, iml, frfr, tofr );
A timeframe is a, possibly non-linear, function of global linear time as determined by m1. Its parameters are a Real3 and another timeframe, thus constituting a recursive hierarchy. So a timeframe is completely defined by a list of
Real3. The timeframe of
m1. The timeframe of
[r3)#tl is a function of the timeframe of tl as follows:
r3'1 are interpreted as a 1-dimesional homogeneous transformation defining a linear function.
r3'2defines a non-linear `acceleration' or `deceleration' in the unit interval. Let a =
r3'0, b =
r3'1, p =
r3'2and t a time-value in the timeframe of tl.
} tlm1( tl, t )
and the inverse:
} itlm1( tl, t )
A timemap based on a timeframe is obtained as:
} m = tlm1( tl, m1() )
It must be noted that timeframes serve at least a double purpose. In the first place they serve to make different time-rates available. In this case the non-linear acceleration is not usable. On the other hand they define a hierarchy of intervals. This aspect is especially exploited in sound synthesis as is explained in the next section.
There is a single call to calculate a `sound image' in analogy of
writeimage. The result is a
m1ds dependent, map, generated from a number of
m1ds dependent maps. The function is
raudio and its arguments are a filename and a number of lists, as many as there are channels required in the resulting sound files.
} m = raudio( name, lleft, lright )
The lists are build as follows:
each element is a list. Let
el be such a list. The first element of
el is a timeframe list. This timeframe indicates the interval and course of time in this interval for the following elements in
el. For each further element of
ml be such an element, then
is a real valued map that is to be evaluated in the interval to contribute to the sound channel
is a list with parameters aimed at changing a map parameter at the time point indicated as the begin point of the interval. The first element of
mlis a map. Let
pmbe this map. The second element is a value or a map. Let
npbe this element.
npis a new parameter for
pm, like a programmed execution of
setmapat this point in time. The index of the parameter in
pmcan be given as a third element of
ml. The default is 0.
An existing file can be opened as map with
There are a couple of functions dedicated to special sorts of sound- synthesis. One example is
fsin, a sine generator which takes a frequency as argument. This is an aid in implementing complex fm synthesizers. The following is an example of the implementation of a fm synthesizer with one carrier and an indefinite number of modulators, each with its own frequency and modulation index.
fmgen = "fsin( rfmgen( 3 ] )"; rfmgen = "mfi = %1; ( mxi = mfi+1 ) > pars() ? return( %2 ) : 0; mf = %mfi; mx = %mxi; rfmgen[ %1+2, %2 + mx*mf*fsin( mf ) ]";
Another example is
delay, a distortion-free delay line with real valued argument, ideal for implementing wave-guide algorithms. There is also an implementation of a
For the generic coupling with (graphical) user interfaces there is a mode that can be switched on with
} uimode( T );
In that case the interpreter stops its normal execution cycle and passes control to some external mechanism by calling the user defined C-function
uiloop. The interpreter can be given control again by calling the C-function:
extern void setuimode( int );
with argument 0, turning uimode off. A callback mechanism is available from the external (user interface) environment with the following means:
extern void inituiargs();
to initialize the arguments for a callback;
extern void adduiarg( void *argp, int argtp );
to push an argument for a callback on the parameter stack;
extern void setuicom( char *name );
to execute a named IL function with the parameters created with
adduiargs. Implementations have been made with this mechanism for the following:
vuit, with automatic generation of the necessary C-code to translate C callbacks to IL callbacks
ILis invoked by executing the command `il' from the operating system shell. Alternatively `ilm' can be executed for the X/Motif version.
il [file ...] [-M dir] [-L dir] [-D dir] [-P dir] [-A dir]
The file(s) are IL source files to be executed before entering the console interpreter. The options control, together with corresponding environment variables, the directories where the interpreter looks for files, or puts them. For each file category the process is the same:
When writing files the first-found (writable) directory is taken.
The file categories, with their option letter, environment variable and default subdirectory name, are:
A list of directories can be specified with an environment variable by separating the directories with a space or a colon `:'.
Debugging facilities become available when linking an executable with a special kernel object file. The debugger can be invoked by a direct call of the implicit function
} ildb() (ildb)
Other ways to get inside the debugger are the occurrence of an error, the invoking of a stop event or the signaling of an interrupt (^ C). The ways commands can be given are very similar to `dbx', the UNIX source level debugger. The possible commands are:
::prefixes. Information about module, local nesting-depth and protection are given.
ildbwhen the global identifier
varis assigned to.
stop in fun
ildbwhen the function
var, printing the function name where this happens.
trace in fun
fun, printing from which function this call is made.
ildb. This is useful when
ildbis invoked with an interrupt and no further continuation is wanted.
attributes of solids
checking for existence
coercion of quaternions
composited image rendering
continuous domains, continuous domains
conversion of domains
copying of lists
copying of values
discrete domains, discrete domains
files for programs
functions, parameter mechanisms
functions, user defined
integrating differential equations
operators, user defined
protection of identifiers
rendering of images
scope of identifiers
strings as functions
* (transform solid)
/ (real unary)
real3, real3, real3, real3