RenderMan Shading Language (RSL) has always had a rich library of built-in functions, already known to the RSL compiler and implemented as part of the runtime shader interpreter in the renderer. This built-in function library includes math operations (sin, sqrt, etc.), vector and matrix operations, coordinate transformations, and higher level functions like noise and texture. New and useful built-in functions have been steadily added with each new PRMan release.
It has also been possible to write arbitrary functions in RSL itself. However, defining functions in RSL has limitations. The RSL compiler inlines a function every time the function is called and thus its code is not shared among its invocations, let alone by separate shaders that might call the same function.
This AppNote shows how PRMan allows one to write new built-in RSL functions in C++. Such functions overcome some of the limitations of RSL-defined functions.
Writing new built-in functions in C++ and linking them as plugins has advantages over writing functions in RSL, including:
Plugins do have some limitations that one should be aware of:
We start out with a simple example for implementing a varying value reduction function, pxGridMax(). It is polymorphic; in other words, you can pass it a float, a point-like type, or a color and it will compute the component wise maximum in a like type. Since the shader compiler prevents a function that takes a varying argument from returning a uniform result, the second argument to the function is an output argument containing the uniform result.
#include <stdlib.h> #include "RslPlugin.h" extern "C" { RSLEXPORT int pxMaxGridFloat(RslContext* rslContext, int argc, const RslArg* argv[]) { RslFloatIter arg (argv[1]); RslFloatIter result(argv[2]); *result = *arg; // Use first value as initial max. int numVals = RslArg::NumValues(argc, argv); for (int i = 0; i < numVals; ++i) { if (*arg > *result) *result = *arg; ++arg; ++result; } return 0; } RSLEXPORT int pxMaxGridTriple(RslContext* rslContext, int argc, const RslArg* argv[]) { RslPointIter arg (argv[1]); // Points and colors are both just float[3] RslPointIter result(argv[2]); // Use first value as initial max. (*result)[0] = (*arg)[0]; (*result)[1] = (*arg)[1]; (*result)[2] = (*arg)[2]; int numVals = RslArg::NumValues(argc, argv); for (int i = 0; i < numVals; ++i) { if ((*arg)[0] > (*result)[0]) (*result)[0] = (*arg)[0]; if ((*arg)[1] > (*result)[1]) (*result)[1] = (*arg)[1]; if ((*arg)[2] > (*result)[2]) (*result)[2] = (*arg)[2]; ++arg; ++result; } return 0; } static RslFunction myFunctions[] = { { "void pxGridMax (float, output uniform float)", pxMaxGridFloat, NULL, NULL }, { "void pxGridMax (point, output uniform point)", pxMaxGridTriple, NULL, NULL }, { "void pxGridMax (color, output uniform color)", pxMaxGridTriple, NULL, NULL }, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions; }; /* extern "C" */
// The plugin directive tells the compiler which files // to search for plugin functions. plugin "pxGridMax"; surface gridmax () { uniform float ymax = 0; uniform point pmax = (0,0,0); pxGridMax(ycomp(N), ymax); pxGridMax(P, pmax); printf ("y = %f, pxGridMax(y) = %f\n", ycomp(N), ymax); printf ("P = %p, pxGridMax(P) = %p\n", P, pmax); }
Plugins must include the provided header, RslPlugin.h, which contains class definitions and macros that are used for writing your own plugin functions. You must compile your plugins with a C++ compiler, but you must use C linkage. To accomplish this, use extern "C" around the tables and functions to guarantee C-style linkage for the plugin loading system.
The C++ file contains a table describing the functions contained in the plugin essentially mapping the RSL functions (with arguments) to the C++ implementations. The functions may be polymorphic; in other words, one may have separate functions of the same name that are distinguished by the types of the arguments passed to them or by the type of the value returned by the function.
To give a more concrete example, here is an example table that describes implementations of a squaring function for several types of arguments:
static RslFunction myFunctions[] = { { "float sqr (float)", sqr_float, NULL, NULL }, { "point sqr (point)", sqr_triple, NULL, NULL }, { "vector sqr (vector)", sqr_triple, NULL, NULL }, { "normal sqr (normal)", sqr_triple, NULL, NULL }, { "color sqr (color)", sqr_triple, NULL, NULL }, { "float sqr (...)", sqr_vararg, NULL, NULL }, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions;
Note the following properties of the table declaration:
Plugin functions are declared as follows:
RSLEXPORT int myfunction(RslContext *rslContext, int argc, const RslArg *argv[]) { ... }
The function takes three arguments: rslContext is a pointer to the thread-specific state in which the plugin function is running, argc is the number of arguments to the function (including one for the return value), and argv is an array of argument pointers (each of which is a const RslArg*). The argument array starts with the return value, if any (argv[0] should not be used if the RSL return type is void). The plugin function returns an integer return value: 0 indicates no error and a nonzero return value indicates an error. The RSLEXPORT macro is an OS-specific declaration used to ensure the function is visible to the program loading the plugin. Be sure to enclose all the plugin functions in an extern "C" { ... } block, as shown in Section 2.
The RslArgs have a number of useful methods used to determine attributes of a given argument. IsFloat(), IsString(), IsPoint(), et cetera can be used to determine if an argument is a given type. IsArray() can be used to determine if an argument is an array or not. If the argument is an array, GetArrayLength() can be used to determine the number of elements of an array argument. See the RSL Plugin API Reference for a description of other advanced methods.
In PRMan a plugin function will operate over a collection of points at the same time. However, not all of the points in the collection will be active at any given time (due to the nature of loops or conditional statements in the shader). In order to perform operations on only those points that are active the plugin interface provides iterators. The data for each active point of a given argument is accessed through an RslIter object of a given type. The example from Section 2 demonstrates how iterators are used:
// Get iterators for result and argument. The result is always uniform. RslFloatIter result(argv[2]); RslFloatIter arg(argv[1]); // Copy the first value (the initial maximum) from arg to result. *result = *arg; // Query for the number of iteration values from the argument. // It will be one if the argument is uniform. int numVals = argv[0]->NumValues(); // Copy the maximum argument value to the result. for (int i = 0; i < numVals; ++i) { if (*arg > *result) *result = *arg; // Increment the argument iterator. Result is uniform, // so it does not require incrementing (but doing so wouldn't hurt). ++arg; }An iterator is constructed from a given argument:
The iterator can then be treated like a pointer to the given type, so one can access the data for a given argument at a given point by simply dereferencing it:
returns the value of the first active float. The iterator is advanced by simply incrementing it:
The pre-increment operator should be preferred, since it is more efficient than the post-increment operator. Assignment to arguments is equally simple:
Note that any argument (not just argv[0]) can be used as an output argument. It is up to the programmer to ensure that plugins only modify assignable values that have been specified with the output modifier on the arguments.
In this new interface, it is crucial to know how many iterations must be performed by the plugin function. The IsVarying() method of the RslIter class can be used to determine if an argument is varying or uniform. For varying arguments, the number of iterations will be equivalent to the number of active points on the grid; for uniform arguments, only one iteration should be performed. The number of iterations to perform can be obtained from the NumValues() method of the RslArg. Functions that return a value in the result argument should use argv[0]->NumValues() as the number of iterations, since the shader compiler guarantees that the detail of the result argument matches the detail of all the other arguments. Functions that return void or return any results in output arguments must examine all their arguments to determine the correct mix of varying and uniform argument iterations. There is a static convenience function in RslArg to simplify this: int numVals = RslArg::NumValues(argc, argv). It should be passed the argc and argv that were passed into the plugin function. As an example, a plugin function that adds its arguments would iterate as follows:
int numVals = argv[0]->NumValues(); for (int i = 0; i < numVals; ++i) { *result = *a + *b; ++result; ++a; ++b; }If the result is varying, the loop will run once for each active point. If the result is uniform, the loop will run just once. If the result argument is varying and either or both arguments are uniform, the loop will still run over all the active points for the result. If the result argument is uniform and either argument is varying, the shader compiler will flag the detail mismatch as an error. An example that uses output arguments, pxGridMad.cpp is discussed in Section 2.
RSLEXPORT int appendTxLeak(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); // WARNING -- THIS WILL LEAK MEMORY! char *tempString = new char[inLen+4]; strcpy(tempString, *inString); // This code assumes the strings have uniform detail tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; *outString = tempString; return 0; } static RslFunction myFunctions[] = { { "string appendTxLeak(string)", appendTxLeak, NULL, NULL}, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions;
Dereferencing a string iterator gives a string pointer (char*). Strings are assigned by pointer assignment (e.g. *outString = tempString). However, the contents of an RSL string should never be modified. Also, the renderer does not free strings that are allocated in plugins, so the above example would cause memory to remain in use for the result until the end of render. One should never free memory associated with a plugin argument; doing so will result in a crash. Storage management is discussed in Section 3.6.
Arrays are accessed through a separate class of iterators, RslArrayIter. Array iterators for all of the types in RSL are provided. Here is an example that searches an array of vectors for a negative z component and returns an array of zeroes and ones based on the results.
RSLEXPORT int findNegZ(RslContext* rslContext, int argc, const RslArg* argv[]) { RslFloatArrayIter results(argv[0]); RslVectorArrayIter vecs(argv[1]); int resultSize = argv[0]->GetArrayLength(); // Walk over all the active shading points int numVals = argv[0]->NumValues(); for (int i = 0; i < numVals; i++) { float* resultArray = *results; RtVector* vecArray = *vecs; // Search the array for -z at this active point for (int j = 0; j < resultSize; j++) { if (vecArray[j][2] < 0) { resultArray[j] = 1.0; } else { resultArray[j] = 0.0; } } ++results; ++vecs; } return 0; } static RslFunction myFunctions[] = { { "float[] findNegZ(vector[])", findNegZ, NULL, NULL}, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions;
As mentioned in the string example above, memory allocated in a plugin method is not freed by the renderer; the plugin developer must perform their own memory management. There are five different ways this can be done: per frame plugin initialization, per frame function intialization, global store allocation, thread-local allocation, and shader-local allocation. The first three are not thread safe and the second two are. For memory management that is not thread safe, the developer must place synchronization primitives around memory accesses to ensure correctness.
Per frame initialization is an easy way to allocate memory for data tables that need to be created exactly once for the life of a plugin; the memory allocated by per frame initialization will persist for the duration of a frame. At the end of the frame, a cleanup routine can be called to free any allocated resources. An example that uses per frame initialization is shown below and also in Section 4.
static char* tempString; initStringBuffer(RixContext *ctx) { tempString = new char[1024]; } deleteStringBuffer(RixContext *ctx) { delete[] tempString; } RSLEXPORT int appendTx(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); // WARNING -- WITHOUT LOCKS, THIS WILL NOT WORK WITH MULTIPLE THREADS! // Need to lock access to tempString here strcpy(tempString, *inString); tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; // Need to unlock access to tempString here *outString = tempString; return 0; } static RslFunction myFunctions[] = { { "string appendTx(string)", appendTx, NULL, NULL }, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions(myFunctions, initStringBuffer, deleteStringBuffer);
This version of the plugin does not leak memory on each execution, but, as noted in the comments, it has a major problem: it is not thread safe. The next example shows the use of per function initialization and uses RixInterfaces to create lock objects that ensure thread safety. However, in the example above, the string from one thread of execution will most likely be copied to strings from other threads, potentially causing an error that would be hard to debug. In addition, this version is not ideal because it assumes the strings coming into the function will never exceed 1024 characters in length.
The example below uses per function initialization to initialize global memory and locks for the given appendTx plugin function. Note however, there are two caveats with per function initialization, the first is that an initialization function will be run for every plugin function it is associated with. So if the same init function is associated with multiple plugin functions, it will be executed multiple times. The second is that cleanup functions are only run on plugin functions that have defined init functions, NULL init function pointers will prevent cleanup functions from executing.
The initializers in this example use RixInterfaces to obtain a factory object called RixThreadUtils. Once we have the RixThreadUtils factory object, we then create an RixMutex object that can be used to lock and unlock access to the shared global data in the plugin. Ideally one would never use locks in a plugin, since they reduce the paralellism that can be achieved by the multi-threaded renderer.
static char* tempString; RixMutex *s_stringLock = NULL; initStringBuffer(RixContext *ctx) { RixThreadUtils *lockFactory = (RixThreadUtils *)ctx->GetRixInterface(k_RixThreadUtils); s_stringLock = lockFactory->NewMutex(); tempString = new char[1024]; } deleteStringBuffer(RixContext *ctx) { delete[] tempString; if (s_stringLock) delete s_stringLock; } RSLEXPORT int appendTx(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); // Need to lock access to tempString here s_stringLock->Lock(); strcpy(tempString, *inString); tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; // Need to unlock access to tempString here s_stringLock->Unlock(); *outString = tempString; return 0; } static RslFunction myFunctions[] = { { "string appendTx(string)", appendTx, initStringBuffer, deleteStringBuffer }, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions;
The final example describes the use of RixInterface objects that can be used to create memory stores that can be shared across plugin functions as well as across plugin files and across plugin types. We now revisit the global appendTx example one last time:
static RixMutex *s_stringLock = NULL; // Called automatically at end of frame void destructGlobals(RixContext *ctx, void *buffer) { delete[] (char *)buffer; if (s_stringLock) delete s_stringLock; } RSLEXPORT int appendTx(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); RixStorage* globalStorage = rslContext->GetGlobalStorage(); // We must lock a global store before operating on it to be thread safe globalStorage->Lock(); char *tempString = (char *)globalStorage->Get("tempString"); if (!tempString) { tempString = new char[1024]; RixThreadUtils *lockFactory = (RixThreadUtils *)rslContext->GetRixInterface(k_RixThreadUtils); s_stringLock = lockFactory->NewMutex(); globalStorage->Set("tempString", (void *)tempString, destructGlobals); } globalStorage->Unlock(); // Need to lock access to tempString here s_stringLock->Lock(); strcpy(tempString, *inString); tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; // Need to unlock access to tempString here s_stringLock->Unlock(); *outString = tempString; return 0; }This example is a little more complex, because it attempts to maintain thread safety. The GetGlobalStorage method of the RslContext allows one to retrieve a global storage object. Since this is a global memory store, one has to lock it before performing operations on it with the Lock method. Then memory associated with an arbitrary key can be obtained with the Get method. If no data has been stored for this key, a NULL value is returned. If one detects that no data has been stored with this key, it can be created and put back into the global memory store with the Set method. A full description of the interfaces available via the GetRixInterface method can be found here: RixInterface Reference
An much simpler and more efficient, thread safe way to write this function involves using the RslContext methods GetThreadData and SetThreadData to manage a memory buffer for each thread of execution. The code to do this is shown here:
// Called automatically when a thread terminates void destructBuffer(RixContext *ctx, void *buffer) { delete[] (char *)buffer; } RSLEXPORT int appendTx(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); char *tempString = (char *)rslContext->GetThreadData(); // If the string is NULL allocate a buffer for this thread if (!tempString) { tempString = new char[1024]; rslContext->SetThreadData((void *)tempString, destructBuffer); } strcpy(tempString, *inString); tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; *outString = tempString; return 0; } static RslFunction myFunctions[] = { { "string appendTx(string)", appendTx, NULL, NULL}, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions = myFunctions;
This version of our string appending function works well it is thread safe and efficient. However the code still expects the strings will never exceed 1024 characters in length. A final revision to this function involves the use of SetLocalData and GetLocalData. These are very similar to the SetThreadData and GetThreadData methods, but the allocated memory persists only for the time that it takes to shade the current collection of points being operated on by the shader. This makes it more efficient in terms of peak memory utilization, but it can be less efficient in terms of the time utilized to call memory allocation and deallocation routines for every collection of points, rather than once per thread. The code for this version of the function is shown below:
// In this version, the destructBuffer function is called // at the end of the shader that uses this function RSLEXPORT int appendTx(RslContext* rslContext, int argc, const RslArg* argv[]) { RslStringIter outString(argv[0]); RslStringIter inString (argv[1]); int inLen = strlen(*inString); char *tempString = (char *)rslContext->GetLocalData(); // If the string is NULL allocate a buffer for this grid if (!tempString) { tempString = new char[inLen + 4]; rslContext->SetLocalData((void *)tempString, destructBuffer); } strcpy(tempString, *inString); tempString[inLen] = '.'; tempString[inLen+1] = 't'; tempString[inLen+2] = 'x'; tempString[inLen+3] = '\0'; *outString = tempString; return 0; }
Compiling and using a new plugin requires three steps:
Compiling your C++ file is straightforward: just use the standard C++ compiler to generate an object (.o/.obj) file, then generate a shared object (.so/.dll) file from the object file. Remember that, though using C++, you must use C style linkage. You also must ensure that your C++ compiler and libraries are compatible with the compiler and runtime libraries used by PRMan (gcc for Linux and OS-X and Microsoft Visual C for Windows). Here are example commands for building a plugin on several architectures:
Linux: g++ -fPIC -I$RMANTREE/include -c myfunc.cpp g++ -shared myfunc.o -o myfunc.so Mac OS-X: g++ -I$RMANTREE/include -c myfunc.cpp setenv MACOSX_DEPLOYMENT_TARGET 10.3 g++ -bundle -undefined dynamic_lookup myfunc.o -o myfunc.so Windows: cl -nologo -MT -I%RMANTREE%\include -c myfunc.cpp link -nologo -DLL -out:myfunc.dll myfunc.obj %RMANTREE%\lib\prman.lib
The resulting file myfunc.so or myfunc.dll is the plugin that implements your new function. It is not important that the filename matches the name of the function.
When compiling your shader, if the RSL compiler comes across a function reference that is neither a known built-in nor a function defined in RSL, it will search all plugins defined with the plugin directive until it finds one within the exported RslPublicFunctions table. Note that the plugin file must be specified with the plugin directive. (The .so or .dll extension is not required.) The path to plugins can be relative and can be modified with the -I command line option of the RSL compiler. The RSL compiler will typecheck your function call and issue a warning if you pass arguments that do not match any of the entries in the RslPublicFunctions table of your plugin.
Once your shader is successfully compiled, you are ready to render. The renderer requires the plugin to be in one of the directories that it searches for shaders. In other words, the searchpath specified with the option Option "searchpath" "shader" also specifies the searchpath for the RSL function plugins.
In 2002, Ken Perlin published a concise paper in Computer Graphics; Vol. 35 No. 3. This paper addressed two issues in the original Perlin Noise algorithm. Below is a plugin that provides a C++ implementation for the improved noise function, based on his java code.
// This code is based on code from Ken Perlin's web page at http://mrl.nyu.edu/~perlin/noise // Improved Noise Copyright 2002 Ken Perlin #include <math.h> #include <stdlib.h> #include "RslPlugin.h" static int permutation[] = { 151,160,137,91,90,15, 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 }; //----------------------------------------------------------------------- // We use a per-frame initalizer to create the table when the plugin is // first loaded in PRMan. Since this table is read only, we do not need // to worry about locking multi-thread access to this global variable. //----------------------------------------------------------------------- static int p[512]; void initNoiseTable(RixContext *ctx) { for (int i = 0; i < 256; i++) p[256+i] = p[i] = permutation[i]; } inline static double fade (double t) { return (t * t * t * (t * (t * 6.0 - 15.0) + 10.0)); } inline static double lerp(double t, double a, double b) { return (a + t * (b - a)); } inline static double grad(int hash, double x, double y, double z) { int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE double u = h<8 ? x : y; // INTO 12 GRADIENT DIRECTIONS. double v = h<4 ? y : h==12||h==14 ? x : z; return (((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v)); } inline static double noise(double x, double y, double z) { int X, Y, Z; int A, B, AA, AB, BA, BB; double u, v, w; X = (int)floor(x) & 255; // FIND UNIT CUBE THAT Y = (int)floor(y) & 255; // CONTAINS POINT. Z = (int)floor(z) & 255; x -= floor(x); // FIND RELATIVE X,Y,Z y -= floor(y); // OF POINT IN CUBE. z -= floor(z); u = fade(x); // COMPUTE FADE CURVES v = fade(y); // FOR EACH OF X,Y,Z. w = fade(z); A = p[X ]+Y; AA = p[A]+Z; AB = p[A+1]+Z; // HASH COORDINATES OF B = p[X+1]+Y; BA = p[B]+Z; BB = p[B+1]+Z; // THE 8 CUBE CORNERS, return (lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD grad(p[BA ], x-1, y , z )), // BLENDED lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS grad(p[BB ], x-1, y-1, z ))),// FROM 8 lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS grad(p[BA+1], x-1, y , z-1 )), // OF CUBE lerp(u, grad(p[AB+1], x , y-1, z-1 ), grad(p[BB+1], x-1, y-1, z-1 ))))); } extern "C" { RSLEXPORT int improvedNoise (RslContext* rslContext, int argc, const RslArg* argv[]) { RslFloatIter retArg (argv[0]); RslPointIter pointArg(argv[1]); int numVals = argv[0]->NumValues(); for (int i = 0; i < numVals; ++i) { *retArg = (float) noise ((*pointArg)[0], (*pointArg)[1], (*pointArg)[2]); ++retArg; ++pointArg; } return 0; } static RslFunction myFunctions[] = { { "float improvedNoise(point)", improvedNoise, NULL, NULL}, NULL }; RSLEXPORT RslFunctionTable RslPublicFunctions(myFunctions, initNoiseTable); }; // extern "C"
Pixar
Animation Studios
|