-
Notifications
You must be signed in to change notification settings - Fork 11
4. Casting and In Expressions
While Gator introduces geometry types to help manage schemes and reference frames, manipulating data to move between these spaces can still be tricky. In this tutorial, we explore casting using in
and as!
expressions, when to use these operations, and introduce some detailed rules about how they work. A longer example of using these to write a lighting model using these operations can be found in examples/auto_phong/fragment.lgl
.
Casting operations simply take a variable of type a
and make that data be type b
. In C/C++, one might write a x = …; b y = (b) x;
and in JavaScript one might write a x = …; b y = <b> x;
. In Gator, the above code would be written as a x = …; b y = x as! b;
. As with other languages, using an as!
expression to cast is a bad idea and should be avoided whenever possible (hence the exclamation mark)!
Gator's in
expressions, on the other hand, actually send the data in a given variable to the correct scheme and/or reference frame, and function the same as applying the operations yourself. This means that in
expressions are actually safe to use in regular code, and are immensely helpful when managing frames and transformations.
Since in
expressions must preserve the geometric object, we simplify the syntax to require that the user only writes the target frame and/or the target scheme. For example, we might send a position pos
to the world frame with pos in world
or to homogeneous coordinates with pos in hom
.
So how does Gator know what operations it can use when transforming data using an in
expression? When writing code, it is possible to label functions and data such as matrices (linear operators) as being canonical transformations, allowing Gator to store these operations for invocation during in
expression typechecking and compilation.
With canon
functions we can, for example, write and use the homify
function to manage transformations to homogeneous coordinates for vectors in the model reference frame:
type vec3 is float[3];
type vec4 is float[4];
... // Standard declarations of cartesian and homogeneous coordinates
declare vec4 vec4(float[3] v, float f); // GLSL-style vector extension
frame model has dimension 3;
canon hom<model>.vector homify(cart3<model>.vector v) {
// Note here the use of 'as!' expressions to tell the compiler these casts are known to be safe
return vec4(v as! vec3, 1.) as! hom<model>.vector;
}
void main(cart3<model>.vector aPosition) {
auto posHom = aPosition in hom; // = homify(aPosition)
}
A notable feature is that canonical functions can be declared across generic functions and parameterized types. For example, we can nicely extend homify
to work across any reference frame:
... // Declarations as above
with frame(3) T:
canon hom<T>.vector homify(cart3<T>.vector v) {
return vec4(v as! vec3, 1.) as! hom<T>.vector;
}
void main(cart3<model>.vector aPosition) {
auto posHom = aPosition in hom; // = homify<model>(aPosition)
}
Canonical functions are transformations between arguments to a single space; however, these transformations must occasionally be augmented with additional information that can't be provided through type parameterization and a static function definition. To provide this information, we allow the introduction of canonical arguments, values labelled as canonical which can be used both as canonical transformation helpers and as regular variables.
So what might a canonical argument look like? Sometimes it is necessary to include another variable in an operation, such as including a matrix when transforming between frames using matrix multiplication. To capture this behavior, arguments to a function can be given the canon
keyword, which simply means that the given argument may be canonical, and selected by inferred operations during in
expression translation. Valid variables are labelled with canon
. For example, we can define matrix multiplication to transform between the model and world frames for points:
... // Standard definitions
with frame(3) r:
scheme cart3 : geometry {
object point is float[3];
with frame(3) r2:
object transformation is float[3][3];
...
with frame(3) r2:
canon this<r2>.point *(canon this<r2>.transformation m, point p) {
return (m as! mat3 * p as! vec3) as! this<r2>.point;
}
}
frame model has dimension 3;
frame world has dimension 3;
void main(cart3<model>.position aPosition, canon cart3<model>.transformation<world> uView) {
auto worldPosition = aPosition in world; // = uView * aPosition
}
Sometimes using in
and as
expressions simply isn't possible or produces unexpected behavior. In this section, we explore errors that can arise using these expressions and some rules about how in
expressions decide behavior.
Unlike casts in other languages, as!
expressions must cast to another type with the same underlying basic type. For example, int x; x as! float
will fail, but the following code will typecheck:
type scalar is float;
type meter is float;
scalar x = 1.;
meter y = x as! meter;
Since in
expressions have geometric meaning, they are much more restrictive. The following requirements are enforced by the typechecker when writing the expression x in b
for variable x
of type a
and type b
:
-
a
must be a geometry type (of the formscheme<frame>.object
) -
b
must denote a scheme and/or a frame (among the formsscheme
,frame
, orscheme<frame>
) - There must be at least one valid canonical path from
a
tob
What is meant by a valid canonical path? This definition is captured by the requirements for canonical functions (functions labelled with the keyword canon
):
- Must take exactly 1 non-canonical argument
- The non-canonical argument and return type must be geometry types
- A generic return type must be dependent on a generic argument type
- The reference frames of the non-canonical argument and return type must have the same dimension
- (Unchecked) must maintain global path independence
These requirements are structured to allow Gator to safely generate transformations between frames and schemes while attempting to prevent some potentially abusive behaviors with in
expressions. The three notably unusual requirements are the need for exactly 1 non-canonical argument, maintaining frame dimension, and global path independence. The need for 1 non-canonical argument captures the intuition that a canonical function is a transformation of one value from one geometric interpretation (geometry type) to another. Maintaining frame dimension is a requirement that avoids pathological violations of path independence, since changes in dimension generally lose or assume information.
Path independence is somewhat out of scope of this tutorial, but the basic principle is that each transformation must agree with all other transformations such that any path between two points is identical. This is categorically the same as saying our graph must always form a commutative diagram. For more information, read the Gator paper; I may also write a follow-up post exploring this idea in more detail. For all practical purposes, ensuring that your transformations are consistent with the basis vector dimensions used (for example), this property should hold. Finally, note that Gator does not check that path independence holds; doing so is an area of current research.