The previous chapter described the large-scale structures of MetaSL that use the shader as a fundamental building block. This chapter proceeds in the opposite direction, starting with the smallest component of a MetaSL program, working up through the hierarchy of language features to present the internal structure of a shader.
| MetaSL uses various data types derived from C++, but also defines types like Color3 and Ray specific to the domain of shader programming. Variable declaration and initialization for scalar and array types are based on the syntax of C++. A constructor notation for the initialization of aggregate types like Color3 is based on C++ class constructor syntax. Like C, aliases for type names can be created using the typedef specifier. |
A value is a piece of data used by MetaSL in its calculations, like a number or a color. A value is characterized by its data type, a category to which the value belongs. These MetaSL data types are more precisely defined than the intuitive concepts on which the data types are based. For example, there is a difference in MetaSL between a number that you would use for counting (1, 2, 3) and a number that contains a fractional part (0.5, 3.14159, 4.2).
A data type is specified by a reserved word, a name that is predefined by MetaSL for use in MetaSL programs. For example, the reserved word int specifies values that are integers. Many of these data-type names are derived from widespread conventions in other programming languages. A typical example is the name float, an abbreviation for “floating-point number.” A float is a number that contains a decimal point. Within the precision of the hardware, a float can have any number of digits on either side of the decimal point, like 127.5 or 1.3333 — the decimal point can “float” to any position among the digits.
A data type that consists of a single type is called a simple data type. Collections of simple data types can define complex data types. For example, a color (in traditional computer graphics implementations) typically consists of red, green, and blue components. A color type can be defined as a complex type by specifying three values of type float for its red, green, and blue components.
A variable is a name defined in a MetaSL program that refers to a value of a certain data type. These three aspects — its name, data type, and value — are used in a variable declaration that creates the variable.
To declare a variable of a simple type, you write the data type and name of the variable, followed by its value. For example, to define a variable of type int named year with a value of 2010, you would write:
Because the integer 2010 is the initial value that the variable will have, declaring a variable with a value is called initializing a variable.
All simple variable declarations can follow this pattern:
The value is preceded by the “equals” character (=) which associates the value with the variable. The declaration ends with the semicolon character (;) that serves the same purpose as a period at the end of a sentence.
MetaSL defines a number of complex data types that are useful in rendering: Color3 for colors, float3 for a set of three floats used for mathematical concepts like points and vectors. You can think of a Color3 type as having a single value that defines a color, or as a set of three values that define the red, green, and blue components. The float3 is just one member of a family of types that are based on the simple type float; a float2 contains two float values, a float4 contains four.
To define a value for the Color3 type, you can specify the red, green, and blue components of the color separated by commas in parentheses after the Color3 data type:
Rather than repeating the Color3 data type, another structure, or syntax, for declaring complex types may also be used. In this syntax, the list of components directly follows the name of the variable:
In these examples of declarations, space characters are added to the declarations to make the syntax clearer. In a MetaSL program, space characters are optional in many cases; you would probably write something more compact:
This structure, in which a name is followed by values separated by commas and surrounded by parentheses, is the same as the syntax you will see for functions in a later section. A function calculates a value based on its arguments, the values in the parentheses. For this form of the declaration of a Color3, the red, green, and blue values in parentheses serve as the arguments to a function that creates a Color3. Because this structure constructs a new Color3 variable, this form is called a constructor for the data type.
As a shortcut for constructors like Color3 in which the same value should be used for all the components of the complex type, you can just specify that single value. In the following declarations, variables gray_1 and gray_2 have the same value: a Color3 with 0.5 for all three components.
Some types are simply a different name, or type synonym, for a base MetaSL type. The Color3 type is a different name for float3 — both contain three floating point values, but Color3 may give the reader of a program a better idea of that variable's intended use. To create a synonym for a type, you use the typedef specifier with the name of an existing type and the synonym you are creating.
The previous simple and complex data types all define a fixed number of components. An array is a complex data type that can contain an arbitrary number of values of the same type. The values that the array contains are called array elements.
To declare a variable to be an array of a certain type, you add the size of the array in square brackets after the name of the variable:
This declaration creates an array without any elements to be used later in a MetaSL program. To define an initial value for the array when it is declared, its value is specified using the “equals” character (=) in the same manner as for the types already described. You list the values to be contained by the array within curly braces ({}) and separated by commas.
An element in an array is described by its position, or index, with the first element having index zero, the second having index one, and so on. Rather than thinking of the index as a number like “first” or “second,” you should think of it as the “distance” from the first element in the array — there are no elements before the first element, so it is element zero; the second one is one element away from the first one, and so forth.
An array can contain values of any type, though all the values in an array must have the same type. For example, an array can contain a series of values of type Color3. To initialize the array, you can use the constructor syntax for Color3 to define the elements in the initialization list.
Declarations and their initial values can extend over more than one line to help clarify their structure. However, the structure of this array of Color3 values is the same as an array of int values: a list in curly braces, with the values separated by commas.
In these examples of array, the size of the array — the number of elements it contains — is part of its declaration. Arrays may be declared without a size, an unsized array. This is useful when the size of the array is not known in advance, for example, when an array is passed as an argument to a function (described in a later section).
In all of the examples above, the numeric values used in initializing variables were specified directly — 2010, 0.8, 0.9, 1.0 — and are therefore called literal values, or simply, literals. Typically, the initial values for variables will be literals. But once a variable has been declared, that variable can be used in the initialization of other variables. For example:
The initial value of next_year in this declaration, year + 1, is an expression, which is the topic of the next section.
| Variables and literals can be combined into expressions using a subset of the C++ operators. Components of complex types can be specified using the name of the field and dot notation from C++ class instances. Arithmetic operations of variables of different simple types perform type promotion to the type of higher precision of the operands. An expression with two operands of the same complex type performs pair-wise operations on the operand's components. Expressions containing complex types will perform promotions if possible; an operation on a simple type and a complex type will replicate the simple type to match the dimension of the complex type. Operator precedence is identical to C++. |
An expression is a value created by combining one or more variables and literals using combinations of symbols like +, *, and / that have meanings defined by MetaSL. Many of the meanings of these symbols are familiar from arithmetic, like the plus (+) character used for addition. Other symbols are based on standard conventions in other programming languages, like the asterisk (*) character for multiplication. Because these operators implement the basic calculations of arithmetic, they are called arithmetic operators. The variables and literals combined by operators are called operands. Most operations combine two expressions that are placed on either side of the operator. The expression on the left side of the operator is called the left operand; the expression on the right is called the right operand.
For example, if the variables width and height have already been declared and initialized, then you would use the multiplication symbol to calculate the area represented by the width and height of a rectangle. In the following expression, the width and height variables are the left and right operands of the multiplication operator.
The result of an arithmetic operation using two numbers is another number. A Boolean expression is a comparison of two numbers and produces a value of either true or false. For example, the character < means “less than,” so the expression 1 < 10 has a value of true, but the expression 3 < 2 has a value of false. The various Boolean operators in MetaSL are a standard set in most programming languages and have the same meaning as you would expect from inequalities in algebra. (The full set of MetaSL operators are listed by their category in the MetaSL reference for operators.)
More complex expressions can be defined by enclosing the sub-expressions to be calculated first in parentheses.
Without parentheses, arithmetic operations are performed in pairs in an order called operator precedence. For example, multiplication and division is calculated before addition and subtraction. Operator precedence means that the parentheses were not strictly necessary in the previous example; the two multiplications would have been performed first, and those two values then added together. However, adding parentheses is always a good idea if you are unsure of the precedence rules or would simply like to clarify the meaning of the expression. (See the precedence table in the MetaSL reference for operators.)
The simple types that compose a complex types can also be used in an expression. To use an array element as a value in an expression, you put the element's index in square brackets after the array name.
Using square brackets to reference an element is the same syntax as the array's size declaration.
Even if the array contains complex types, the array indexing is done in the same manner.
Complex types like Color3 and float3 have predefined names for their components. For Color3 , the red, green, and blue components are called r , g , and b ; for float3, the components are called x , y , and z , to represent a point in three-dimensional space.
To refer to a component in an expression, you follow the variable name with a period character (. ) and the name of the component. For example, the blue component of a Color3 variable called tint would be tint.b . A component of a complex type specified in this manner can be used in an expression in the same way as a simple type. For example, the luminance of a color is a single value derived from the different sensitivities of the human eye to red, green, and blue. The MetaSL expression for luminance for a color named tint would be:
The types of the values and variables in an expression must make sense for the operation being performed. For example, MetaSL also defines a String type for variables with a value containing text. Multiplying a float value by a String value does not have a reasonable interpretation, and is therefore not allowed. However, multiplying an int by a float is possible; the int is first converted to a value of type float, and then the two float values are multiplied.
For an expression with types for which conversion is defined, values are converted to the type with the highest precision of all the values in the expression. For integers and floating-point numbers, the increase in precision to that of a float is obvious; you can't express a number between two and three with an integer.
For two values with the same complex type, an arithmetic operation is the result of performing that operation on the separates components, matched by name. For example, if you have declared two Color3 variables like this:
...then multiplying the two Color3 variables:
...would result in three multiplications, one for each component:
Typically, if an arithmetic operation makes sense for the components of a complex type, then that operation will also be legal for the complex type itself.
In the same way that a value of type int was converted to a float when it was multiplied by a float, simple types may be implicitly converted to a complex type in an expression. For example, multiplying a Color3 value by a float value first converts the float value into a value of type Color3 that uses the float value for all three of its components.
For example, if you had declared variables darken and light_blue in this way:
Then this expression in which you multiply the two variables:
...is equivalent to multiplying the variables as if they had both been declared as Color3 variables instead.
All of the previous expressions have consisted of literals and variables that are combined using operators that represent arithmetic and Boolean operations. In a MetaSL program, the value of an expression is the result of replacing all the variables with their values, and using those values as operands. Determining the value of an expression is called evaluating the expression. Evaluating a literal produces that literal value; evaluating a variable produces that variable's value; and evaluating two expressions combined with an operator is the result using that operator with the evaluations of the two operands.
However, by only using the operators describe so far, there is no way to retain the result of evaluating an expression. This is made possible by the assignment operator, represented by the equals character (=). The assignment operator redefines the value of a variable given as the left operand to be the value of the expression given as the right operand.
The value of the assignment operator is the value that is assigned to the left operand. Because assignment is an expression, it can be combined with other expressions. For example, two variables can be modified at the same time by using an assignment expression as the right operand of another assignment expression.
The assignment to variable height is made first, yielding the value 100.0, which is then assigned to variable width. When more than one assignment operator occurs like this in an expression, the evaluation is grouped to the right — the assignment operator is said to be right-associative.
The left operand for assignment must behave like a variable — it must be able to store a value. An expression that can store a value is called an lvalue — short for “left value” — because it occurs on the left side of the assignment operator. Conversely, the kind of expression that can occur on the right side of the assignment operator is called an rvalue — for “right value.” An lvalue is a more restricted type of expression than an rvalue; only variables and the components of variables can be lvalues, but any expression can be evaluated as a potential rvalue.
From these examples of the way that MetaSL uses the “equals” character you can also see that it has a different meaning than in algebra. In algebra, “=” means “is equal to”; in MetaSL, it means “now has the value of.” (This convention for the meaning of the “equals” character is used in many programming languages.)
Because of the different meaning that the “equals” character has in a programming language compared to algebra, an assignment expression that modifies an integer variable named counter in this way:
...is meaningful in MetaSL — it replaces the previous value of counter with one greater than that value. The MetaSL expression cannot be read as if it were an algebraic expression, however; there is no possible value of counter for which the expression is true. But based upon the definition of the operands of the assignment operator, the difference with algebra becomes clearer: here the variable counter is being used both as an rvalue and as an lvalue.
This modification of the left operand in an assignment expression is the first example so far of a change to the value of a variable once it has been initialized. This change of state is called a side effect of the evaluation of the assignment expression. The side effects of expression evaluation are retained through a grouping structure in MetaSL called the statement, described in the next section.
| Statements in MetaSL implement variable assignment, conditional execution, and iteration (for, while, and do/while) using C++ syntax. A sequence of statements enclosed by curly braces is treated as a single statement in the same contexts as in C++. MetaSL also provides additional iteration constructs using the foreach statement for light-emitting scene-data objects and for correlated sampling in one to four dimensions. |
You can think of MetaSL's reserved words and operators, along with the variables you declare, as the vocabulary of MetaSL. Statements in MetaSL use that vocabulary to specify an action that a MetaSL program will perform when it is executed. Like the sentence in human languages, a statement is the smallest structure that can stand on its own. A MetaSL program consists of a sequence of statements; this section describes the various types of MetaSL statements.
In declaring a variable, you specify an initial value by following the variable name with an “equals” character (=) and the value:
The variable name, equals character, and value have the same structure as the assignment operation described in the previous section. However, the role of a declaration is to create a new variable and give it a value. The assignment operator replaces the current value of a variable with the value of the right operand. To create a statement from the assignment expression, you simply add the semicolon character to the end of the expression. As in a variable declaration, the semicolon functions like a period at the end of an English sentence.
Creating a statement from an assignment expression is one example of an expression statement. The syntax for an expression statement is quite simple — a semicolon is added to the end of an expression. However, unless the expression has a side effect, the execution of an expression statement will not produce a value that is available in other statements later in the program.
At this point, an expression statement that performs variable assignment may seem to duplicate the function of variable declarations. The usefulness of assignment expressions will become clearer in the more complex statements of the next sections.
How can you calculate the sum of the seven prime numbers contained in the array primes? An obvious method is to create an expression in which all the elements are added together.
This method is only convenient if the size of the array is small and is impossible if the number of elements in the array is not known in advance. If, however, the sum is calculated using seven statements in the following way, a pattern is displayed that suggests how the original expression can be simplified.
In the seven statements that incrementally modify the variable sum, the only varying quantity is the array index. In MetaSL, the for statement can represent the repeated modifications to the variable sum in a single statement. A structure that implements repetition in programming language is often called a loop, so the for statement is also called a for loop. Because the for statement does not perform a single action but directs the flow of control of the program, a for statement implements control flow in MetaSL.
The for statement specifies a set of statements that should be repeated and defines the repetition: how variables used by those statements should be initialized, how they should be modified for each repetition, and when the repetition should stop. These defining components of the loop follow the reserved word for and are enclosed in parentheses. The statements to be repeated follow the defining components and are enclosed in curly braces. This group of statements is called the body of the loop.
For example, to add up the values of all seven elements of the array primes, the for statement should:
The initialization takes the form of a variable declaration. The ending test is a Boolean expression — the repetition stops when the expression has a value of false. The repeated action is a statement containing an assignment expression. Assuming that the variables primes and sum have declared as above, the for statement to calculate the total of the array elements contains a single statement that updates sum as the running total.
The structure of the for statement is defined by the position of the parentheses and curly braces. The semicolon separates the expressions that define the behavior of the repetition. Such characters, used to create boundaries within the code, are called delimiters.
MetaSL provides shorter forms for several types of statements that contain assignment expressions. For example, modifying a variable by using its value in an operation has a shorter form in which the variable is not repeated.
In the calculation of the sum of array elements, the sum variable is updated in this way.
Adding a value of one to a variable is another typical operation. The briefer form eliminates the “equals” sign.
The ++ syntax is useful for adding one to a variable that is used as an index into an array in the loop body.
MetaSL defines other methods for repeating a set of statements that will be described in examples developed in a later section. However, the structure of many other parts of MetaSL are represented in this small example: declarations, statements, delimiters, and the flow of control.
The for loop uses a Boolean expression for a very specific purpose: to terminate the execution of the loop. The if statement uses a Boolean expression in a more general way: to decide whether or not a series of statements should be executed. Like delimiters in the for loop, the controlling information — the Boolean expression — is surrounded by parentheses, followed by a set of statements — the clause of the if statement — contained within curly braces.
The Boolean expression and the clause of the if statement use variables declared before the if statement. If the value of the Boolean expression is true, then the statements in the clause are executed. In the following example, the two assignments to the red and green components of the Color3 variable background are only executed if the value of the variable scale is greater than 1.0.
An if statement can be followed by another reserved word in MetaSL, else. After the else another set of statements are enclosed in curly braces. These statements are only executed if the value of the Boolean expression is false, that is, if the first set of statements were not executed. These two groups of statements are often called separately the if clause and the else clause.
In practice, the two clauses of the if/else form of the if statement may manipulate the same variable, depending upon the value of the Boolean expression.
These examples of the if statement are contrived to simplify their explanation; the value of the variable scale is already known, so why would you need an if statement dependent upon it? Using variables with values that can vary and can't be known in advance is what makes functions useful, described in the next section.
| Functions are defined using standard C++ syntax for the return type, function name, the parameter list, and function body. However, passing values back from the function using pointers is not allowed. An extension to the C++ function syntax uses the keywords in, out, and inout to specify the copy semantics of function parameter passing, thereby implementing multiple return values from functions. |
So far, the words used for the elements of MetaSL have been a mixture of terms and concepts borrowed from linguistics (“statement,” “declaration,” “clause”) and algebra (“expression,” “variable,” “Boolean”). Another concept from algebra that is fundamental to many programming languages is that of a function. In algebra, a function defines a relationship between numbers. For example, a function that produces even numbers can be written as follows:
The letter f represents a relationship that associates, or maps, any positive integer x to the positive integer 2(x-1). For example, the first even number is zero, the second is two, and the fifteenth is twenty-eight.
Another way of looking at a function that is useful in a programming language like MetaSL is that it represents a process in which an input is transformed into an output.
In this view, the function f takes one number as an input and produces another number as an output.
A function can be characterized by the type of input it requires and the type of output it will produce. Note that for this function to produce an even number, the input must be an integer greater than zero. With function f, any number — negative or with a fractional component — will produce an output value, but only with integers greater than zero does the function produce even numbers (that are integers by definition).
Other functions may be limited in the input values for which the function can produce an output value. For example, a function that divides one by its input value cannot have an output value for an input value of zero — dividing by zero is undefined.
The restrictions on the input to a function corresponds to the limitations on the output a function can produce.
The definition of a function in MetaSL makes the kinds of input and output that are appropriate for the function part of the function definition. It describes what the function does using the variable declarations, statements, and delimiters already shown in the previous MetaSL examples.
To define a function in MetaSL, the concept of “input” will be generalized to include any number of input values.
In the MetaSL definition of a function, space characters and delimiters divide the function code into four parts:
Parentheses are used to enclose the parameters; curly braces enclose the statements in the function body.
The function body can contain all the various structures of MetaSL for variable declaration, assignment, looping, and conditional execution. For example, a simple function that multiplies two input values defines its four parts as follows:
The function can be displayed as a process with inputs and outputs.
The value “returned” by a function is defined with the return statement and follows the return reserved word in the function's body.
The value defined by the return statement can be an arbitrary expression. The multiply function can be simplified by returning the multiplication of the parameters directly.
Though the multiply function is trivial — it is just a lengthy replacement for the multiplication operator — it will be useful in showing how a function can be part of an expression.
The parameters of the multiply function are the float variables value_1 and value_2. When the function is defined, those variables do not have values; they are used symbolically in the statements in the function body. When the function is used in an expression — when you call the function — those symbolic variables are given actual values, called the function's arguments, which are then used to calculate the function's return value.
For example, the argument values can be floating point literals in an expression used to initialize a variable.
Any expression that results in a value of the correct type can be used as an argument.
The multiply function returns a value of type float. Because multiply also has parameters of that type, a call to multiply can be used as an argument to another call to multiply.
Here the distinction between “parameter” and “argument” helps explain what values the function can use when it is called. The function's definition specifies the number and type of its parameters; when the function is called, the arguments — the actual values used in the function body — must match the parameters. “Matching” in this case also includes the possibility of a conversion of an argument to the type required by the function; an int used for a float parameter would be converted to a float before it is used in the statements of the function. This conversion is permitted because the float type is typically of higher precision than the int type.
These examples show that there are two simple rules for calling a function:
Only the return type and its parameters must be considered to determine if a function can be used in an expression. The contents of the function's body — its implementation — is not relevant in this determination as long as the correct value is returned. Because of this, the function's return type, name, and parameters fully characterize the function, and are therefore called its signature. Conversely, the implementation in the body is encapsulated, hidden from the rest of the program. Encapsulation allows you to improve the implementation of a function without regard to where the function is called as long as you do not change the function's signature.
Functions that you write can contain all the MetaSL features described in the previous sections — variable declarations, statements, and flow-of-control structures like if/else and the for loop.
As part of the language, MetaSL also provides a set of pre-defined functions, called the standard library functions. These functions implement various mathematical operations, for example, finding the square root of a number or determining the sine and cosine of an angle. You can use these functions in the same way as the functions that you have defined.
Most of the standard library functions also can be called with arguments of various types, similar to the way that the addition operator (+) can be used with values of type int, float, float3, and so forth. A function that is defined for different types of arguments is said to be overloaded. The same kind of pair-wise calculation performed for operators will be done by functions that are defined in this way.
For example, if variables p and q have been declared like this:
...then the result of the min function using p and q as arguments:
...is exactly the same as calling the min function on each of the components and combining the result into a float3:
In both cases, r is equal to float3(0.2, 0.1, 0.8).
The full set of standard library functions is described in the MetaSL standard library reference.
All of the simple functions described so far produce a single result and can therefore be part of any expression wherever a value of the function's return type can be used. However, it may be convenient to calculate and return more than one value from a function. A small addition to the syntax of the function's parameter list provides this feature.
In a function definition, a parameter is treated as a local variable, a variable that only has meaning within the body of the function.
By default, any modification of the parameters in the function body will not affect a variable that was supplied as an argument when the function was called.
For example, if the multiply function modified the param_1 parameter, like this:
...then the value of a variable supplied for parameter param_1 will not be modified, even though param_1 is modified inside the function:
MetaSL implements passing values to functions through parameters by copying argument values. The value of an argument is copied into the function as the value of the corresponding parameter in the function body. This is why the value of variable var_1 in the previous example was not modified; once the value of var_1 had been copied for use as the value of parameter param_1, the relationship between var_1 and param_1 was over.
This copy behavior is the default for function parameters. The parameter syntax in the previous function definitions is a shortcut for explicitly declaring the parameters to have this behavior using the parameter qualifier that means “copy the value to the parameter in the function.” The qualifier is the reserved word in, which precedes the parameter data type. A parameter qualified by in is called an input parameter.
To modify a variable supplied as an argument in a function call, MetaSL provides another type of copy operation, defined with the parameter qualifier out. If a parameter in a function definition is preceded by out, then the value of the parameter at the end of the function body is copied back to the variable used for that parameter in the function call. A parameter qualified by out is called an output parameter.
For example, the following function, add, defines one output parameter and two input parameters. Because the resulting value of the function will be stored in the variable passed as the result parameter, the function itself will not return a value using the return statement. To declare that a function will not return a value, the reserved word void is used as the function's return type.
The copy qualifiers define the time at which the copying process occurs between the parameters of the function definition and the variables of the function call. The input parameters are copied before the statements of the function body begin execution; the output parameters are copied after the function body completes execution but before the function returns.
Because the first parameter of function add uses the out qualifier, calling add will modify a variable passed as the first argument.
The argument passed to an output parameter must be able to have an assignment made to it – it must be an lvalue (as described in the section “Assignment expressions”). Because the value of an input parameter is simply copied into the function, no such restriction applies to it. Arguments supplied for input parameters can be any expression appropriate for the parameter type that can be assigned to a variable, including both rvalues and lvalues — literals, variables, and compound expressions. When an lvalue is supplied as an argument for an input parameter, only its value is used; the assignment capability of an lvalue is not relevant in that case.
To define a function parameter that combines the copy behavior of both input and output parameter, the parameter type is preceded by the inout qualifier, defining an input/output parameter. A variable used for an input/output parameter has its value copied into the function, and can therefore be used in expressions in the statements of the function body. After the last statement of the function body, the current value is copied back to the variable, exactly like an output parameter.
An input/output parameter allows a function to behave like the shortcut assignment operators (for example, += and *=) in which the value of a variable is used to modify it (the variable is used both as an rvalue and as an lvalue).
Two of the standard library functions, sincos and modf, make use of output parameters. Function sincos calculates both the sine and cosine of an angle. (It may be more efficient for MetaSL to calculate them at the same time, and both may be useful in any case.) In the following example, the standard library function radians converts the angle measure in degrees to a measure in radians, the kind of measurement required by function sincos.
No value is returned by sincos — it only uses two output parameters to return the sine and cosine of the angle provided as the first argument. Because no value is returned, the return type of sincos is void; you can see this in the signature of sincos.
In contrast, the standard library function modf has one output parameter, but also returns a value. The modf function splits a number into two parts: the integral (whole number) part, and the fractional part. By returning the fractional part as the value of the function, modf can be used in an expression. However, the integral part may also be useful, and can also be acquired in a single call to modf because of the output parameter.
Throughout the rest of the book, the standard library functions will play an important role in the implementation of the set of functions that define a shader, the topic of the next section.
| A shader in MetaSL is a plugin module for a rendering system, having the object-oriented conventions of member functions and variables. A shader defines zero or more input values and at least one output value. The primary method, called “main,” defines the output value of the shader. |
A shader uses all the general programming structures described so far to define the primary structure in MetaSL specifically designed for rendering systems. The name “shader” is misleading, however. Though the first user-defined components of rendering systems were limited to determining the surface color (“shading”), the term is now widely used to describe a variety of user-customizeable components of a rendering system. MetaSL shaders can customize other aspects of the rendering besides shading — the behavior of the virtual camera lens, the simulation of spotlights, the attenuation of light in a volume of smoke.
The syntax of a shader resembles the declaration of a variable: the reserved word shader is followed by the shader's name. However, the syntax of a shader definition also resembles the definition of a function; after the shader name, the shader body follows, surrounded by curly braces, and ending with a semicolon.
Within the shader body, sections of declarations and functions are separated by labels. The labels consist of the words “input,” “output,” or “member”, each followed by a colon character. The input and output section contain variable declarations. The member section contains variable declarations and functions specific to the shader. The variables declared in a shader are called member variables. The functions defined in a shader are called methods.
The input section can contain values to be used by the shader in its calculations, called its input parameters, analogous to the parameters of a function. Each input parameter also specifies a default value, identical in form to the value supplied for a variable when it is initialized. The output section declares the variables that define one or more of the shader's result values, its output parameters. The member section must contain at a minimum a method called main which is responsible for calculating those outputs.
The member section of this simple shader contains the single main method. The output calculation it performs is trivial — it simply provides the single input value as the shader's single result.
The main method has the typical structure of a function, with some restrictions on how it can be defined.
There can be any number of input parameters. In the following shader, called Blend, two Color3 parameters and one float parameter are used to blend between two colors, based on the weighting value defined by the float parameter called color_1_fraction.
The Blend shader demonstrates all the fundamental parts of a shader: the input parameters, the output parameters, and the function that calculates the shader's resulting value. The next chapter builds on this basis to show more complex shader techniques and the pictures they can create.