RiceScript is a custom programming language derived from C and GLSL. If you know any C-family language, you should be already familiar with the syntax.

It is designed for straight-forward translation to GLSL or Javascript. In the future, it is planned to implement translation to asm.js / web assembler to further improve performance on the web.

RiceScript is in the early stage of development, therefore many basic features are not supported, like pointers or dynamic memory allocation. However, thanks to its simplicity, it integrates well with GLSL, for example: you can use GLSL-like math in RiceScript or even re-use parts of RiceScript code in GLSL.
Quick comparison to C

As you can see in the example below, RiceScript look and feels like C, with few minor exceptions, i.e. unnamed "()" function and operator ^ that is calculating power instead of xor:
() // declaration of RiceScript-specific unnamed "main" function,
   // called every frame
{
   cls(0); // clear screen to black
   
   int n = 6;
   double s = 2^n; // operator ^ in RiceScript is power, not xor
   
   double t = klock(); // get current time in seconds
   
   // xres, yres are global variables, "double" type
   // xres = width in pixels of the current window
   // yres = height in pixels of the current window
   
   for(double x=0; x<xres; ++x)
   {
      double y = yres/2;

      y += turbulence(x/s, t, n)*s;
      
      setpix(x, y); // draw single pixel, white color by default
   }
}

double
turbulence(double x, double y, int n) 
{
   double r = 0;
   double a = 1, s = 1;
   for(int i=0; i<n; ++i, s*=2, a*=.5)
   {
      r += noise(x*s,y*s)*a; // calls built-in perlin noise function
   }
   return r;
}
  
RiceScript is also introducing some other domain-specific idioms that are covered in the next paragraphs.
Primitive types

There are two primitive types in RiceScript:
  • double - IEEE-754 64bit floating point number
  • int - 32bit signed integer, internally can be represented as double (in Javascript translation)
GLSL vector/matrix types, functions and operators can be enabled with a macro:
    #use glsl_math
Here is a list of supported types:
  • float - IEEE-754 32bit floating point, internally can be represented as double (in Javascript translation)
  • vec2 - floating point 2-tuple { x, y }
  • vec3 - floating point 3-tuple { x, y, z }
  • vec4 - floating point 4-tuple { x, y, z, w }
  • mat2 - floating point 2x2 matrix
  • mat3 - floating point 3x3 matrix
  • mat4 - floating point 4x4 matrix
Unnamed functions and default return type

As shown in the previous example we can declare unnamed functions, which usually denotes main function/starting point of a program. The default return type for all functions is "double" and can be omitted in function declaration. For unnamed functions, return type is always "double" and cannot be explicitly declared. For other functions we can optionally declare return type: for now only primitive types and vector/matrices are supported. References in a return type are not supported.

In PolyCube, the main function is declared simple as "()", like in EvalDraw, and is called every frame. Other EvalDraw modes: (x), (x,y), (x,y,&r,&g,&b), etc.. are not supported. Unlike in EvalDraw language, the main function itself and scope after main function has to be explicity declared:
()
{
   // some code here that will be called every frame
}
  
Other functions can be declared in the global scope only - it is not possible to declare function inside another function. The default return value is 0 and "return" keyword at the end of function is optional. However, if we want to return different value than 0 it has to be preceeded with "return" keyword (unlike in EvalDraw, where it is enough to type variable name or constant expression).
Auto variables and scope of declared variables

The default numerical type for variables is "double". In declaration of static variables or in function arguments double keyword can be omitted.
Moreover, numerical variables don't have to be declared. Undeclared numerical variables (non-static) are automatically declared as auto variables in the main scope of the function. Other variables: static or with explicity declared type will be called declared variables.

It is important to note that unlike auto variables, declared variables have restricted scope visibility, just like in C langauge; they are only visible in the scope of declaration and after the declaration. This is the main difference to "eval" language used in EvalDraw, where all variables are declared and visible in the main scope of the function.

Let's compare explicitly typed and non-typed version (both will compile in RiceScript):

// Explicitly-declared variable types

()
{
   cls(0);
   
   int n = 6;
   double s = 2^n;
   
   double t = klock();
   
   for(double x=0; x<xres; ++x)
   {
      double y = yres/2;
      y += turbulence(x/s, t, n)*s;
      
      setpix(x, y);
   }
}

double
turbulence(double x, double y, int n) 
{
   double r = 0;
   double a = 1, s = 1;
   for(int i=0; i<n; ++i, s*=2, a*=.5)
   {
      r += noise(x*s,y*s)*a;
   }
   return r;
}
  
// Auto variables with "double" type

()
{
   cls(0);
   
   int n = 6;
   s = 2^n;
   
   t = klock();
   
   for(x=0; x<xres; ++x)
   {
      y = yres/2;
      y += turbulence(x/s, t, n)*s;
      
      setpix(x, y);
   }
}


turbulence(x, y, int n) 
{
   r = 0;
   a = 1; s = 1;
   for(int i=0; i<n; ++i, s*=2, a*=.5)
   {
      r += noise(x*s,y*s)*a;
   }
   return r;
}
  

Now, let's look closely at non-typed version and note down variables visibility:
()
{
   cls(0);
   
   int n = 6; // "int" type has to be explicitly declared
   s = 2^n; // "s" is auto "double" variable, "^" is power operator
   
   t = klock(); // "t" is auto "double" variable
   
   for(x=0; x<xres; ++x) // "x" is auto "double" variable
   {
      y = yres/2; // "y" is auto "double" variable

      y += turbulence(x/s, t, n)*s;
      
      setpix(x, y);
   }
   
   // "x" and "y" are visible outside of "for" loop,
   // even that they are only used inside!
}

// default return type is "double"
turbulence(x, y, int n)
   // arguments "x", "y" have default type "double"
{
   r = 0;
   a = 1; s = 1;
   // "r", "a", "s" are auto "double" variables
   
   for(int i=0; i<n; ++i, s*=2, a*=.5)
   {
      r += noise(x*s,y*s)*a; // calls built-in perlin noise function
   }
   
   // "i" is declared, thus not visible outside "for" loop

   return r;
}
  
Please note that auto-variables are supported in RiceScript mostly for EvalDraw compatibility, but developers are encouraged to use C/GLSL-compatible syntax instead.
GLSL translation and #use macro

In GLSL shaders we can declare #use macro as follows:
    #use function_name
  
Where "function_name" is either a built-in functions (i.e. noise function) or our custom function declared in RiceScript that we want to translate to GLSL automatically.

Currently #use macro is experimental feature and many restrictions apply. A good practice is to make RiceScript functions that we want to use in GLSL simple and more GLSL-like.

Let's go back to our example. Thanks to #use macro, we can declare "turbulence" function once in RiceScript and call it from GLSL:
()
{
   t = klock(); // get current time in seconds

   gluniform1f("t", t); // pass "t" as uniform

   glquad(1); // draw full-screen quad with alpha = 1.0
   
   cls(); // clear 2d canvas, no color = make it transparent
   
   s = 2^6; // hardcode number of turbulence octaves to 6
   
   for(x=0; x<xres; ++x)
   {
      y = yres/2;
      y += turbulence(x/s, t)*s; // call shared "turbulence" function
      
      setpix(x, y); // draw single pixel, white color by default
   }
}

turbulence(x, y)
   // return type will be automatically "double" in RiceScript and "float" in GLSL,
   // even if we explicitly declare it as "double"
{
   r = 0;
   a = 1; s = 1; 
   // "r", "a" and "s" type will be "double" in RiceScript and "float" in GLSL
   
   // To make translation compatible with OpenGL ES 2.0 shaders,
   // we have to use only simple "for" loops,
   // with explicitly declared type and constant number of iterations
   for(int i=0; i<6; ++i)
   {
      r += noise(x*s,y*s)*a;
      
      // We have to take extra expressions out of "for" body as well,
      // GLSL in WebGL 1.0 require this
      s *= 2;
      a *= .5;
   }
   return r;
}

//====================================================
// After @f we can start unnamed GLSL fragment shader

@f

// First, we have to declare all dependencies, here to built-in noise function
#use noise

// Declare that we will use our "turbulence" function
#use turbulence

uniform float t; // current time in seconds

void main()
{
   const float s = 1.0/128.0; // scale, make it 1/pow(2, 6 octaves + 1)

   float r = turbulence(gl_FragCoord.x*s, gl_FragCoord.y*s - t);
                  // call "turbulence" function declared in RiceScript
   
   gl_FragColor = vec4(r*.25 + .25); // set fragment color
}
  
As you can see, we had to use simpler "for" loop due to OpenGL ES 2.0 shader restrictions. Another thing to keep in mind is that all double-precision floating point numbers will be converted into single-precision float in GLSL.
Go back to main page.
Gallery     Docs     Tutorial     RiceScript    
Follow
   
Feedback
polycu.be (C) 2015
Sign out