Just some points to get you going with programming Mental Ray. There is a lot more to the language than this; you should get to know it better on your own to write efficient codes.


Variable types

type size explanation example
char 1 byte used to store ascii character or small integer
(-128 -> 127)
char char1 = 'N'
int 4 bytes integer (-2147483648 -> 2147483647) int bignum = 1250251
unisgned int 4 bytes unisgned integer (0 -> 4294967295) unsigned int hugenum = 10000000
float 4 bytes floating-point number (3.4Eħ38) float mynum = 3.14159265
double 8 bytes larger or more precise float (1.7Eħ308) double precisenum = 0.000000001

You can use typedef to rename a type. For example,

typedef char smallint; smallint myint = 4;

You can also force a value to become another type, in order to assign it to a different type of variable, for example. You do this by using a mechanism called type-casting:

int num = 2; double sqroot = sqrt((double) num);

Note: we have to cast num into a double here because the sqrt function only accepts a double argument. This little example also shows that C is a stricter language than MEL (for example) when it comes to types; you must make sure you use the right types of variables and values in your procedures.


Array

Array size must be fixed at compile time.

You can initialize an array thus:

int myarray[4] = {1, 2, 3, 4};

To assign values to an array, you must do it one element at a time:

myarray[0] = 9; myarray[1] = 8; myarray[2] = 7; myarray[3] = 6;

The name of an array is a pointer to its first element.

int myint = *(myarray+1); // result: 8

Enumeration

  enum choices {tiny, small, big, huge};

After the above statement, you can create variables of type choices

  enum choices myvar;

and assign either tiny, small, big or huge to it. Actually these values are just named integer constants, implicitly defined as 0, 1, 2, 3... starting from the leftmost item in the enumeration list. You can choose to set any number explicitly, as in

  enum choices {tiny, small = 10, big = 3, huge};

In this case tiny = 0, small = 10, big = 3 and huge = 4.

Incidentally, you can define the enum and create a variable of it at one go:

  enum choices {tiny, small, big, huge} myvar;

Static variable

Static local variables are local variables that keep their contents between calls. For example,

#include <stdio.h>

void fn()
{
  static int count = 0;
  count++;
  printf("%d ", count);
}

void main()
{
  int i;
  for(i=0; i<10; i++) fn();
}

will print "1 2 3 4 5 6 7 8 9 10".

Static global variables are variables that are visible only in the file in which they reside. For example, you could have two files,

/* file 1 */
int num;

void main() {num = 23;}
/* file 2 */
static int num;

void main() {num = 41;}

The num in file 2 is different from the num in file 1, and there wouldn't be a conflict when you link the two files -- they wouldn't link if num wasn't static in file 2 because that would mean a double declaration, which is not allowed in C.

You're advised to avoid statics in your shader programs because they might not go well with multi-threaded machines. Also, you shouldn't use printf() if you plan to share your shader across a network.


Const variable

A const variable is one which you must initialize, and which initial value cannot be changed:

  const int count; /* error, not initialized */
  const int count = 100;
  count = 50; /* error, value cannot be changed */

Pointer

int *p; /* declaring a pointer to an integer */
int myint = 10;
p = &myint; /* assigning myint's address to p */
int anotherint = *p; /* assigning the value 10 to anotherint */

Arrays can be accessed via pointer methods (the array's name is the name of the pointer to the start of the array):

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = a; /* p will point to the address where 1 resides */

Pointer is probably the most confusing topic in C.

int **p; /* pointer to pointer can be used to access 2D arrays */

int *list[2] = {1, 1}; /* array of pointers to integers */

int (*p)[2]; /* pointer to an array of 2 integers, can also be written as int *(p[2]) */

int (*fn)(); /* pointer to a function that returns an int */
fn = myfunc; /* myfunc is a function name, without brackets it represents the function's address */

int *fn(); /* function that returns a pointer to an int */

const int *i; /* pointer to a const, same as int const *i */

int q = 10;
int *const i = &q; /* const pointer to an int */
*i = 0; /* address cannot be changed but value pointed to can be changed */

const int *const j = &q; /* both address and value pointed to cannot be changed */

Dynamic allocation

The standard way to allocate memory dynamically in C is though the malloc() function (and a matching free() function to free the allocated memory). Mental Ray also provides the following functions for these tasks:

  void *mi_mem_allocate( const int size )
  /* Allocates size amount of memory. Returns a pointer to the allocated memory. If allocation fails,
     an error is reported and Mental Ray will try to recover memory or abort it if this fails. The allocated
     block is filled with zeroes, so you cannot see its previous content. */

  void *mi_mem_reallocate( void *const mem, const int size )
  /* Changes the size of an allocated block of memory. The first argument is a pointer to the old block of
     memory, and the second argument is the requested new size of that block. A pointer to the new block is
     returned, which may be different from the the pointer to the old block. Extra bytes in expanded blocks
     are zeroed. */

  void mi_mem_release( void *const mem )
  /* Frees a block of memory pointed to by mem. This function doesn't return anything. */

You should use these functions rather than the standard malloc()/free() because they have built-in memory leak tracing, consistency checking and error handling.


Function

Before you use a function, you must declare it. Declaring a function specifies its return type and parameters. You can either define the function at the start:

double volume( double x, double y, double z )
{
  return x * y * z;
}

void main()
{
  double vol = volume( 3.4, 5.5, 6.7 );
}

or specify a function prototype at the start and define the function itself afterwards:

double volume( double x, double y, double z );

void main()
{
  double vol = volume( 3.4, 5.5, 6.7 );
}

double volume( double x, double y, double z )
{
  return x * y * z;
}

Arguments are normally called by value:

#include <stdio.h>

void change(int var) {var++;}

void main()
{
  int var = 5;
  change(var); /* var is unchanged */
  printf("%d", var);
}

This calling method makes local copies of the variables passed in as arguments. The function body only sees these copies, not the original vraiables.

You can also call the arguments by reference, meaning you pass in the arguments as pointers:

#include <stdio.h>

void change(int *var) {(*var)++;} /* the brackets bind * closer to var */

void main()
{
  int var = 5;
  change(&var); /* var is changed */
  printf("%d", var);
}

Many of the functions provided by Mental Ray use this method of argument-calling.

We've met a function pointer briefly earlier. Here's a bit more about how you can use it:

int sum(int a, int b) {return a + b}

void main()
{
  int (*p)(int a, int b);
  int result;

  p = sum;
  result = (*p)(10, 20); /* result gets 30 */
}

Headers and libraries

In addition to the standard headers, you need to include shader.h (mind the directory path!) in any shader you write. When you compile your shader (see below), you also need to link your shader object file with shader.lib. Both shader.h and shader.lib can be found in [Mental Ray installation directory]\src\shaders\public-baseshaders; you can copy and rename the files shader.[version].h and shader[versionplatform].lib.


Miscellaneous preprocessors

You know about using the #include directive to bring in a header file:

#include <mylheader.h> /* search in standard directories */

#include "myheader.h" /* search in current directory first, then the standard directories */

There's also the #define directive to create a macro name:

#define MYCONST 45.678

void main()
{
  float var = MYCONST * 2; /* MYCONST will be replaced with 45.678 by compile time */
}

You can also use #define to create a function-like macro:

#define SUM(i, j) i + j

void main()
{
  int sum = SUM(10, 20);
}

Note: use #undef to undefine a macro.

There's a bunch of directives (#if, #else, #elif, #endif, #ifdef, #ifndef) to effect conditional compilation.

  #if constant-expression
    statement-sequence
  #endif

statement-sequence will be compiled if constant-expression is true. As this is a preprocessor directive, you cannot use variables in constant-expression.

You can build a conditional ladder using #elif and #else:

  #if constant-expression-1
    statement-sequence
  #elif constant-expression-2
    statement-sequence
  #elif constant-expression-3
    statement-sequence
  ...
  #else
    statement-sequence
  #endif

You can also use a macro as more than a string substitution device -- you can use it directly to steer the compilation process:

  #if defined (macro1)
    statement-sequence1
  #endif

  #if !defined (macro2)
    statement-sequence2
  #endif

statement-sequence1 will be compiled if macro1 is defined, while statement-sequence2 will be compiled if macro2 is not defined. These directives are commonly used to customize the code based on different operating environments.


Structure, union and bit-field

A structure is a package of different variables. Use the . operator to access individual members of a structure. For example,

/* set up a structure template; the name "person" is called the tag of the structure */

struct person {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
}; /* notice the trailing semi-colon! */

void main()
{
  /* declare a structure variable of type person */
  struct person manager;

  /* assign values to the members */
  manager.name = "Chen";
  manager.addr = "23 Dingfuzhuang";
  manager.postcode = 1025;
  manager. id = "100347";
  manager.salary = 2000;
}

You can also declare one or more variable names with the template:

struct person {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
} manager1, manager2;

Or omit the tag if you only need to have one variable of that type:

struct {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
} manager;

Structures can be nested:

struct date {
  int day;
  int month;
  int year;
};

struct person {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
  struct date birth;
  struct date hired;
};

To access a member of a nested structure, use the . operator twice. For example, employee.birth.year.

A union is a collection of variables, just like a structure, except its elements share the same memory location. Its definition resembles a structure's:

union person {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
};

A bit-field is a special kind of structure element that's measured in bits. It's commonly used to pass switch conditions around in a compact way.

struct tag {
  type name1: number_of_bits;
  type name2: number_of_bits;
  type name3: number_of_bits;
} variable(s);

where type must be either int, unsigned or signed. 1-bit fields must be unsigned. The names are optional; unnamed fields can be used to fill up bits that don't need to be accessed. The specified number of bits, not the type, determine the storage size for each field. So for example you can only store the values 0, 1, 2 or 3 in a 2-bit field.

Normal elements and bit-fields can co-exist in a structure:

union person {
  char name[NAMESIZE];
  char addr[ADDRSIZE];
  int postcode;
  char id[9];
  double salary;
  unsigned sex: 1;
  unsigned tall: 1;
};

Note: you can't take the address of a bit-field, thus you can't make an array out of it.


Pointer to structure

If you have a pointer to a structure, you can access the structure members through the pointer using an arrow operator:

struct person employee;
struct person *personpt = &employee;
personpt->postcode = 1234;

Compiling your program

First, as mentioned earlier, copy shader.[version].h and shader[versionplatform].lib from [Mental Ray installation directory]\src\shaders\public-baseshaders to the directory where your shader source file resides. Then rename them to shader.h and shader.lib respectively.

Assuming your shader source file is called myshader.c, run the following command (make sure the VC++ executable directory is in your PATH environment variable):

cl /c /MD /nologo myshader.c

The /c option tells cl to compile without linking; /MD creates multi-threaded dll; /nologo prevents the compiler from printing stuffs like copyright information, compiler version, etc.

Then immediately run another command:

link /nologo /DLL /nodefaultlib:LIBC.LIB myshader.obj shader.lib

The /DLL option creates a dynamically linked library, and /nodefaultlib:LIBC.LIB prevents conflict with the LIBC.LIB library.

On completion of the linking, you'll have a file called myshader.dll (myshader.lib, myshader.obj and myshader.exp can be discarded). Move it to your shader repository.