PRMan ではこうだ、ああだ、なんて云っていますが、 lucille では当然 RenderMan の大きな特徴であるシェーダ言語は搭載していません。固定のシェーディングパイプラインの構成になっています。
PRMan 11 が出るまでは、グローバルイルミネーションをどうやってシェーディング言語に組み込めばよいかという問題がありました(独自拡張形式でやるというのも一つの手です)。
PRMan 11 の登場により、グローバルイルミネーション効果をとりあえずはうまくシェーダに組み込めるという枠組みができたような気がします。
将来的にはサポートしたいシェーダ言語の機能ですが、とりあえずはどんな風に実装したらよいかというのを考えてみました。
本家 PRMan では、シェーダプログラムを、シェーダコンパイラで .slo 形式にコンパイルします。この .slo 形式はテキスト形式で、中間言語で構成されています。これは、いわゆるスタックマシンや java のような仮想マシン環境が解釈できるフォーマットになっています。
これをレンダリング時に PRMan 側が読み込み、必要なシェーディング変数がセットされた環境で中間言語を実行し、シェーディングの結果を生成します。
この中間言語形式は、多くのオープンソースの RenderMan 系レンダラでも実装されているので、実装する上で非常に参考になります。
現在は無くなってしまった Exluna 社の Entropy では、
シェーダプログラム -> C++言語にコンバート -> C++コンパイラでコンパイル -> ネイティブコードで実行
ということもできました。この場合C++の機能が使えるのでいろいろな拡張が行えます。
また、PRMan には、 dso機能というものがあり、これは、RIBの読み込み時にジオメトリを外部のネイティブコード(ダイナミックライブラリ)で作成・処理したり、シェーダプログラムから、Cで書いたルーチンを呼び出すということができます。
直感的には、中間言語形式よりもネイティブコードのほうが処理が早いと思われますが、PRMan の場合、シェーダエンジンは十分に最適化されているので、中間言語もネイティブコードも速度的には変わらないそうです。
実装側としては、RenderMan シェーダ言語を実装するよりも、シェーディングに必要なデータと関数だけ用意しておいて、シェーディングのコードはプラグイン形式でダイナミックライブラリとして読み込んで実行というのが一番簡単で、かつシェーダを書くほうも RenderMan シェーダ言語の枠に囚われずに自由に拡張できるので、そっちの方がやりやすいかなと考える次第です。なんかプロダクションとかでも dso シェーダを多く使っているみたいだし。
将来的には、レンダラの必要なコンポーネントだけを用意しておいて、レイトレとかもシェーディングとかもフォトンマッピングとかも全部プログラマブルでユーザが自由に組み合わせて構築できるような、そんなプログラマティックなレンダリングシステムができないかなと思います。
houdini のレンダラ版みたいな感じで。
そろそろ簡単なシェーダ言語を組み込みたいと思い、いろいろなシェーダ言語の形態を調べています。
今回は mental ray のシェーダアーキティクチャについて。
実は、1年くらい前にシェーダ言語の実装に役立ちそうだと思って、 programming mental ray vol.2 を買っていました。
2万円と結構高いですが、メンタルレイのシェーダアーキティクチャの部分だけでなく、シーン記述やグローバルイルミネーションのパラメータ設定、ジオメトリハンドリングの部分でも役に立つ情報が載っていますので、レンダラ書きには有益な本だと思います。付録にメンタルレイのシーン記述言語の文法ファイルが yacc(bison) 形式で掲載されていますので、自分のレンダラに独自のシーン記述フォーマットを作りたいという人にも有益だと思います。
とは云うものの、実際にメンタルレイを持っているわけでもなく、数ページみただけで、ずっとお蔵入りになっていました...
で、今回ひっぱりだしてきたわけですが、いやーやっぱり商用だけあっけ API まわりとかきっちりしていますね。 lucille にもいずれ参考にしなくては。
メンタルレイのシェーダアーキティクチャ
シェーダ言語の実装をどうするか? やっぱり先人に学べ! ということで、RenderMan 以外のメジャーであるレンダラ、メンタルレイのシェーダの仕組みについて調べてみました。
以下はメンタルレイ本を見ての記述であり、最近のメンタルレイとは違ったりするかと思いますのでご了承ください。
メンタルレイでは、シェーダ言語というのはなく、シェーダは、C/C++ で記述します。
必要となるデータ型や関数は、メンタルレイ側が提供します。
このシェーダコードを、通常の C コンパイラでダイナミックリンク形式にコンパイルして使用します(cのソースコードを直接メンタルレイが受け取って、メンタルレイが内部でコンパイルするオプションもあります)。
また、シーン記述ファイル内には、シェーダとのインターフェスとなるメタデータ構造(使用する変数の名前やデフォルトのパラメータ値など)を記述します。
たとえば、フォンシェーダは以下のようになります。
#include <mi/shader.h>
struct phong {
miColor ambient;
miColor diffuse;
miColor specular;
};
int phong_version(vois) { reutrn(1); }
miBoolean phong(
miColor *result,
miState *state,
struct phong *paras)
{
miColor kd;
*result = *mi_eval_color(¶s->ambient);
...
result->r += kd.r;
result->g += kd.g;
result->b += kd.b;
return (miTRUE);
}
シーン記述ファイル内での、このフォンシェーダのメタデータ定義は、以下のようになります。
declare shader
color "phong" ( color "ambient",
color "diffuse",
color "specular")
version 1
end declare
...
shader "somematerial" "phong" (
"ambient" 0.3 0.3 0.3
"diffuse" 0.5 0.5 0.5
"specular" 0.2 0.2 0.2)
RenderMan と大きく異なるのは、メンタルレイのシェーダは、入れ子状にして階層化することができる点です(シェーダツリー、シェーダDAG)。たとえば上記の phong シェーダの場合だと、 "diffuse" のパラメータに、ほかのシェーダを指定し、そのシェーダが出力した結果の値を割り当てることができます。
また、メンタルレイには、シェーダの上位となる、フェノメナ(phenomenon)という概念があります。メンタルレイ本によると、フェノミナはシェーダの概念を統一するもので、マテリアルやライト、ジオメトリなどの各種シェーダを統括して扱うことができるそうです。
いろいろなオープンソースレンダラのシェーダ言語コンパイラや、Cgコンパイラ、 OpenGL 2.0 シェーダ言語のコンパイラを調べてみたところ、だいたい以下のような実装のアプローチを採っています。
o コンパイラコンパイラのツールとしてレキサ(lexer, 字句解析器)には flex(lex), パーサ(parser, 構文解析器)には bison(yacc) を用いています(これはシェーダ言語でない通常のコンパイラでもほとんどデファクトスタンダードとして使用されている)。
o コンパイラは、ソース -> テキストの中間言語(独自のアセンブラ形式など) を生成する。ネイティブのオブジェクトコード形式ではない。
o レンダラの実行時に、中間言語を、仮想マシン(Virtual Macine)やスタックマシン(Stack Macine) で実行する。つまりは Java と似ているアプローチ。
lucille のアプローチ
lucille でも同様に中間言語方式で、中間言語へのコンパイラと、その実行環境を提供してもよいのですが、 メンタルレイの C/C++ シェーダ形式も魅力的だと考えています。メンタルレイでは、レンダラが RC(Rendering Core)関数と呼ばれるコンポーネントを提供しており、シェーダからこの関数にアクセスすることができます。RC関数には、たとえば
mi_trace_eye()
mi_photon_light()
などのように、機能がモジュール化されています。mi_trace_eye() はレイトレーシングを行い、mi_photon_light() はライトからフォトンを放出します。
RC関数のようにレンダラのコアモジュールを提供することで、RC関数を組み合わせて自由なレンダラ(レイトレースレンダラ、フォトンマップレンダラ、パストレースレンダラなど)をシェーダベースでも作ることができそうです。
RenderMan シェーディング言語はそれほど複雑でないので、C/C++ のコードに変換するトランスレータを書くのは、中間言語の実行環境を書くのと同じかもしくはそれよりも楽かと思います。
そこで、 lucille では以下のアプローチを採ろうかと考えています。
o シェーダ言語は C/C++ で記述(基本的に以下のトランスレータで自動生成)
o RenderMan シェーディング言語から C/C++ シェーダコードへのトランスレータも提供する。
2番目の項目についてですが、RenderMan シェーディング言語 -> 中間言語の方法でも、RenderMan シェーディング言語 -> C/C++ コードの方法のどちらでも、いずれにせよ
同じような RenderMan シェーディング言語のレキサとパーサ(以下まとめてパーサ)を書く必要があります。パーサを書くのはとくに難しくはありませんが、結構書く量は多く、地道な作業になります。
パーサ作成1
偉大なる先人たちのソースコードを参考にしながら、すこしづつ RenderMan シェーディング言語のパーサを作成していきます。
ツールには、flex と bison を用います。flex と bison についてはネットや書籍などで調べて下さい。
最も簡単なパーサ
まず、最も簡単な例として、サーフェスシェーダのスケルトンとなる null.sl シェーダ
surface
null()
{
}
を処理するパーサを書きましょう。
まず、レキサ部分 tut1.l は以下のようになります。
%{
#include "y.tab.h"; /* tokens */
%}
IDENTIFIER [a-z][a-z0-9]*
%%
"surface" { return(SURFACE); }
{IDENTIFIER} { return(IDENTIFIER); }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
これは、文字列ストリームから、"surface" のトークンもしくは任意の文字列(IDENTIFIER)にマッチするものを切り出します。
パーサ部分 tut1.y は以下のようになります。
%token SURFACE
%token IDENTIFIER
%%
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' ')'
'{' '}'
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
ここでは、surface、任意の文字列、左カッコ("(")、右カッコ(")")、左中カッコ("{")、右中カッコ("}") となる構文のみを受け入れます。入力の構文が正しければ、なにも出力されず、構文が間違っていると、なんらかのエラーが出力されます。
flex と bison を以下のようにして呼び出します。
$ flex tut1.l
これにより、lex.yy.c が生成されます。
$ bison -y -d tut1.y
これにより、 y.tab.h(シンボル定数テーブルのリスト) と y.tab.c が出力されます。
これを、 Cコンパイラでコンパイルします(gccなど)。
$ gcc y.tab.c lex.yy.c -lfl
-lfl は flex ライブラリとのリンクです。システムによっては -ll だったり、指定しなくてもよいかもしれません。パーサプログラムができました。 null.sl を渡して、なにも出力されなければ成功です。
$ ./a.out null.sl $
もし、
$ ./a.out tut1.sl parse error
とかでるようだと、入力の構文がおかしいことになります。
このようにして、RenderMan の標準シェーダ群を1個づつ処理していき、すこしづつパーサを完成させていきます。
次回は constatnt シェーダのパースです。
続いて、 null シェーダの次に最も単純な constant シェーダをパースできるようにします。
surface
constant()
{
Oi = Os;
Ci = Os * Cs;
}
constant シェーダをパースするには、"=" による式の代入構文と、 "*" 演算子による2項演算の構文を処理するように拡張を加えます。
レキサ tut2.l は以下になります。
%{
#include "y.tab.h" /* tokens */
%}
IDENTIFIER [a-zA-Z_][a-zA-Z_0-9]*
%%
"surface" { return SURFACE; }
{IDENTIFIER} { printf("ident = %s\n", yytext);
return IDENTIFIER; }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
Oi, Os などの大文字の任意の識別子に対応できるようにIDENTIFIERを変更しています。
また、これからちょっとの間、 printf 文で識別子の出力をしてパーサの動きを理解しやすくします。
パーサ tut2.y は以下になります。
%token SURFACE
%token IDENTIFIER
%right '='
%left '*'
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' ')'
'{' statements '}'
;
shader_type : SURFACE
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : assignexpression ';'
;
/* --- expressions --- */
assignexpression : IDENTIFIER '=' expression
;
expression : primary
| expression '*' expression
;
primary : IDENTIFIER
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
構文定義の名前や構成は、RenderMan Interface 仕様書(バージョン3.2)を参考にしていますので、そちらと見比べることをお勧めします。
演算子の順位
まず、 %right, %left ですが、これは演算子の優先順位を指定する指示子です。
%right は、"=" が右結合(right associativity)であることを指示し、これは最も優先順位を低くすることを意味します。
%left は、"*" が左結合(left associativity)であることを指示し、これは最も優先順位を高くすることを意味します。
このようにするのは、
Ci = Os * Cs;の部分で、Ci = Os が先に処理されてしまうと困るからです。
statements
statements 構文定義では、中カッコ内のシェーダ本体の文の構文を担います。
statements は、空の行(null シェーダのようにシェーダの本体の文が無い)か、もしくは複数の statement 構文から成り立ちます。つまりは本体の文は、 0 行から任意の行までから成るということになります。
statement は、 assignexpression(代入式) と セミコロン ";" で構成されることを指示しています。つまりはセミコロンで終わる行です。
assignexpression は、識別子(IDENTIFIER)、"="、expression(式) から成り立ちます。
さらに、 expression は、
primary(単項式)、もしくは
expression "*" expression(式 掛ける 式)
になります。ここで、 expression の構文定義に expression があるので、ループするんじゃないのかと思いますが、 式 * 式 の結果もまた 式 ということになるのが分かるかと思いますので、これは OK になります。
primary は、1つの識別子(IDENTIFIER) になります。今回は、 Oi, Os, Ci, Cs がこれに相当します。
yydebug
main() 内に、yydebug = 1 がコメントされています。 yydebug は、bison が定義している変数です(bison に -t オプションを付けると有効になる)。このコメントを取ると、パーザがデバッグモードで動作し、詳細な情報を出力してくれます。パーザの動作を検証するときやデバッグ時に役に立ちます。
コンパイル
前回と同じようにして、bison と flex を呼んでプログラムを作ります。
$ flex tut2.l $ bison -y -d -t tut2.y $ gcc lex.yy.c y.tab.c -lfl
そして、constant.sl を渡してみて、以下のようになれば成功です。
$ ./a.out constant.sl ident = constant ident = Oi ident = Os ident = Ci ident = Os ident = Cs出力を見ると、入力のデータを順番に読み込んでいることがわかります。
続いて、 matte シェーダ matte.sl をパースできるようにします。
surface
matte( float Ka = 1;
float Kd = 1;)
{
normal Nf = faceforward(normalize(N), I);
Oi = Os;
Ci = Os * Cs * (Ka * ambient() + Kd * diffuse(Nf));
}
新しく追加するのは、
o シェーダの引数(float Ka = 1; など)
o 関数呼び出し(faceforward()など)
o 変数の定義(normal Nf)
o 加算演算("+")
です。
レキサ tut3.l は以下になります。
IDENT [a-zA-Z_][a-zA-Z_0-9]*
NUM [0-9]+
%%
"float" { return FLOAT; }
"normal" { return NORMAL; }
"surface" { return SURFACE; }
{IDENT} { printf("ident = %s\n", yytext);
return IDENTIFIER; }
{NUM} { printf("num = %d\n", atoi(yytext));
return NUMBER; }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
トークン "float", "normal"を追加します。
また、数字の切り出しも行うようにします(NUM, 今回は整数のみ)
パーサ tut3.y は以下になります。
%token SURFACE
%token IDENTIFIER
%token NUMBER
%token FLOAT NORMAL
%right '='
%left '+'
%left '*'
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' formals ')'
'{' statements '}'
;
shader_type : SURFACE
;
formals : /* empty */
| formal_variable_definitions
| formals ';' formal_variable_definitions
| formals ';'
;
formal_variable_definitions : typespec def_expressions
;
variable_definitions : typespec def_expressions
;
typespec : type
;
type : FLOAT
| VECTOR
;
def_expressions : def_expression
| def_expressions ',' def_expression
;
def_expression : IDENTIFIER def_init
;
def_init : /* empty */
| '=' expression
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : variable_definitions ';'
| assignexpression ';'
;
/* --- expressions --- */
expression : primary
| expression '+' expression
| expression '*' expression
| '(' expression ')'
;
primary : NUMBER
| IDENTIFIER
| procedurecall
| assignexpression
;
procedurecall : IDENTIFIER '(' proc_arguments ')'
;
proc_arguments : /* empty */
| expression
| proc_arguments ',' expression
;
assignexpression : IDENTIFIER '=' expression
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
formals
まず、 shader_definition に、 引数を処理する formals 構文を追加します。
formals は、";" で区切られた任意の数の formal_variable_definitions を受け入れます。
formals_variable_definitions は、 typespec(変数の型)と def_expressions の組になります。
def_expressions では、"," で区切られた任意の数の def_expression を受け入れます。
def_expression は、識別子(IDENTIFIER)と def_init の組で、 def_init は、何も無いかもしくは、 "=" 式(expression) を受け入れます。
formals で受けいれられる構文の例は以下のようになります。
float Ka vector var float Ka; float Ka, var1; float Ka; float Kd; float Ka; float Kd, var1; vector var2, var3;statement
statement にも、変数定義の構文 variable_definitions を追加します。これは、 formal_variable_definitions と同等です。
expression
expression に、加算の処理を追加します。
また、
Ka * ambient() + Kd * diffuse(Nf)
はカッコでくくられているので、カッコでくくられた式の処理も追加します。カッコでくくられた式は加算演算子や乗算演算子よりも高い優先順位を持ちます。
加算演算子 "+" も、 %left に加えます。ここで、%left は、下になるほど優先順位が高くなるので、 "*" の上の行に "+" を加えます。
procedurecall
primary には、関数呼び出し構文を処理する procedurecall を追加します。
procedurecall は、識別子、"("、proc_arguments(引数リスト)、")"の順になっている構文になります。
proc_arguments は、"," で区切られた任意個の式(expression) から成り立ちます。
procedurecall が受け入れる構文の例は、以下のようになります。
ambient() faceforeard(N) function(a, b, 2) function(a * b, (c), d=5, func2(e))
コンパイル、実行
いつもと同じように、コンパイルします。
$ bison -y -d -t tut3.y $ flex tut3.l $ gcc lex.yy.c y.tab.c -lfl
実行して、以下のように出力されれば成功です。
$ ./a.out matte.sl ident = matte ident = Ka num = 1 ident = Kd num = 1 ident = Nf ident = faceforward ident = normalize ident = N ident = I ident = Oi ident = Os ident = Ci ident = Os ident = Cs ident = Ka ident = ambient ident = Kd ident = diffuse ident = Nf
次回は、 metal シェーダです。
続いて metal シェーダ metal.sl です。
surface
metal(float Ka = 1;
float Ks = 1;
float roughness = .1;)
{
normal Nf = faceforward(normalize(N), I);
vector V = -normalize(I);
Oi = Os;
Ci = Os * Cs * (Ka * ambient() + Ks * specular(Nf, V, roughness));
}
今回は、
o 実数
o 単項負演算子( -normalize(I) )
o vector トークン
の処理です。
レキサ tut4.l は以下になります。
%{
#include "y.tab.h" /* tokens */
%}
IDENT [a-zA-Z_][a-zA-Z_0-9]*
exp [eE][-+]?[0-9]+
NUM [-+]?([0-9]+|(([0-9]+)|([0-9]+\.[0-9]*)|(\.[0-9]+)){exp}?)
%%
"float" { return FLOAT; }
"vector" { return VECTOR; }
"normal" { return NORMAL; }
"surface" { return SURFACE; }
{IDENT} { printf("ident = %s\n", yytext);
return IDENTIFIER; }
{NUM} { printf("num = %f\n", atof(yytext));
return NUMBER; }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
NUM は、C言語などでの実数表現を受け入れるように変更しています。例えば以下のような文字列が数値として認識されます。
1.0 0.2 .1 1.0e12 -2.3E-6
"vector" トークンの処理を追加します。
パーサ tut4.y は以下になります。
%token SURFACE
%token IDENTIFIER
%token NUMBER
%token FLOAT NORMAL VECTOR
%right '='
%left '+'
%left '*'
%left UMINUS /* unary minus */
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' formals ')'
'{' statements '}'
;
shader_type : SURFACE
;
formals : /* empty */
| formal_variable_definitions
| formals ';' formal_variable_definitions
| formals ';'
;
formal_variable_definitions : typespec def_expressions
;
variable_definitions : typespec def_expressions
;
typespec : type
;
type : FLOAT
| NORMAL
| VECTOR
;
def_expressions : def_expression
| def_expressions ',' def_expression
;
def_expression : IDENTIFIER def_init
;
def_init : /* empty */
| '=' expression
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : variable_definitions ';'
| assignexpression ';'
;
/* --- expressions --- */
expression : primary
| expression '+' expression
| expression '*' expression
| '-' expression %prec UMINUS
| '(' expression ')'
;
primary : NUMBER
| IDENTIFIER
| procedurecall
| assignexpression
;
procedurecall : IDENTIFIER '(' proc_arguments ')'
;
proc_arguments : /* empty */
| expression
| proc_arguments ',' expression
;
assignexpression : IDENTIFIER '=' expression
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
まず単項負演算子を処理するために、 %left UMINUS を最も下に配置します。単項負演算子(符号の逆転)は、乗算演算子("*") よりも高い優先順位を持つからです。UMINUS には対応するトークンの定義がない仮想的なものですが、これは次で説明します。
expression には単項負演算子を処理する構文
'-' expression %prec UMINUS
を追加します。 %prec UMINUS が追加されています。 %prec は、この構文が UMINUS と同じ優先順位を持つ、と云うことを指定します。そのため、 UMINUS は演算子の優先順位を参照するためだけに存在すればよいので、実体のトークンがない仮想的なものになっているのです。
最後に、 type に VECTOR トークンを追加します。
コンパイル、実行
今まで通りにコンパイルします。
$ bison -y -d -t tut4.y $ flex tut4.l $ gcc lex.yy.c y.tab.c -lfl
実行して以下のように出力されれば OK です。
ident = metal ident = Ka num = 1.000000 ident = Ks num = 1.000000 ident = roughness num = 0.100000 ident = Nf ident = faceforward ident = normalize ident = N ident = I ident = V ident = normalize ident = I ident = Oi ident = Os ident = Ci ident = Os ident = Cs ident = Ka ident = ambient ident = Ks ident = specular ident = Nf ident = V ident = roughness
次回は shinymetal.sl です。
続いて、 shinymetal シェーダ shinymetal.sl のパースです。
surface
shinymetal(float Ka = 1;
float Ks = 1;
float Kr = 1;
float roughness = .1;
string texturename = "";)
{
normal Nf = faceforward(normalize(N), I);
vector V = -normalize(I);
vector D = reflect(I, normalize(Nf));
D = vtransform("current", "world", D);
Oi = Os;
Ci = Os * Cs * (Ka * ambient()
+ Ks * specular(Nf, V, roughness)
+ Kr * color environmentmap(texturename, D));
}
今回は、
o "string" トークン
o ダブルクォーテーションで囲まれた文字列
o 型キャスト( color environmentmap() )
o テクスチャ関数( environmentmap() )
の追加です。
レキサ tut5.l は以下になります。
%{
#include "y.tab.h" /* tokens */
%}
IDENT [a-zA-Z_][a-zA-Z_0-9]*
exp [eE][-+]?[0-9]+
NUM [-+]?([0-9]+|(([0-9]+)|([0-9]+\.[0-9]*)|(\.[0-9]+)){exp}?)
STR \"([^\"\n]|\"\")*\"
%%
"float" { return FLOAT; }
"vector" { return VECTOR; }
"normal" { return NORMAL; }
"color" { return COLOR; }
"string" { return STRING; }
"environment" { return ENVIRONMENT; }
"surface" { return SURFACE; }
{IDENT} { printf("ident = %s\n", yytext);
return IDENTIFIER; }
{NUM} { printf("num = %f\n", atof(yytext));
return NUMBER; }
{STR} { printf("str = %s\n", yytext);
return STRINGCONSTANT; }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
STR は、ダブルクォーテーションで囲まれた文字列にマッチするようになっています。
"string", "environmentmap" を処理するトークンが追加されています。
パーサ tut5.y は以下になります。
%token SURFACE
%token IDENTIFIER
%token NUMBER
%token STRINGCONSTANT
%token FLOAT NORMAL VECTOR COLOR STRING
%token ENVIRONMENT
%right '='
%left '+'
%left '*'
%left UMINUS /* unary minus */ TYPECAST
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' formals ')'
'{' statements '}'
;
shader_type : SURFACE
;
formals : /* empty */
| formal_variable_definitions
| formals ';' formal_variable_definitions
| formals ';'
;
formal_variable_definitions : typespec def_expressions
;
variable_definitions : typespec def_expressions
;
typespec : type
;
type : FLOAT
| NORMAL
| VECTOR
| COLOR
| STRING
;
def_expressions : def_expression
| def_expressions ',' def_expression
;
def_expression : IDENTIFIER def_init
;
def_init : /* empty */
| '=' expression
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : variable_definitions ';'
| assignexpression ';'
;
/* --- expressions --- */
expression : primary
| expression '+' expression
| expression '*' expression
| '-' expression %prec UMINUS
| '(' expression ')'
| typecast expression %prec TYPECAST
;
primary : NUMBER
| STRINGCONSTANT
| IDENTIFIER
| texture
| procedurecall
| assignexpression
;
typecast : COLOR
;
assignexpression : IDENTIFIER '=' expression
;
procedurecall : IDENTIFIER '(' proc_arguments ')'
;
proc_arguments : /* empty */
| expression
| proc_arguments ',' expression
;
texture : texture_type
'(' texture_arguments ')'
;
texture_type : ENVIRONMENT
;
texture_arguments : expression
| texture_arguments ',' expression
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
texture
まず、 primary に、文字列定数 STRINGCONSTANT と、texture を追加します。
primary ...
| STRINGCONSTANT
| texture
...
...texture : texture_type
'(' texture_arguments ')'
;
texture_type : ENVIRONMENT
;
texture_arguments : expression
| texture_arguments ',' expression
;...
expression
expression には、型キャスト構文を加えます。
typecast expression %prec TYPECAST
型キャストは、%prec TYPECAST を使用することで、 UMINUS(単項負演算子) と同じ優先順位を持たせるようにしています。
コンパイル、実行
いつもと同じようにコンパイルします。
$ flex tut5.l $ bison -y -d -t tut5.y
実行して、以下のようになれば成功です。
ident = shinymetal ident = Ka num = 1.000000 ident = Ks num = 1.000000 ident = Kr num = 1.000000 ident = roughness num = 0.100000 ident = texturename str = "" ident = Nf ident = faceforward ident = normalize ident = N ident = I ident = V ident = normalize ident = I ident = D ident = reflect ident = I ident = normalize ident = Nf ident = D ident = vtransform str = "current" str = "world" ident = D ident = Oi ident = Os ident = Ci ident = Os ident = Cs ident = Ka ident = ambient ident = Ks ident = specular ident = Nf ident = V ident = roughness ident = Kr ident = environmentmap ident = texturename ident = D
つぎは、 plastic.sl のパースです。
つづいて、 plastic シェーダ plastic.sl のパースです。
surface
plastic( float Ka = 1;
float Kd = .5;
float Ks = .5;
float roughness = .1;
color specularcolor = 1;)
{
normal Nf = faceforward(normalize(N), I);
vector V = -normalize(I);
vector D = reflect(I, normalize(Nf));
Oi = Os;
Ci = Os * ( Cs * ( Ka * ambient() + Kd * diffuse(Nf))
+ specularcolor * Ks * specular(Nf, V, roughness));
}
plastic シェーダには新しい要素はないので、前回までのパーサで処理することができます。
...と終わってしまったので、次の(そして最後の)シェーダである paintedplastic シェーダ paintedplastic.sl のパースに進みます。
surface
paintedplastic( float Ka = 1;
float Kd = .5;
float Ks = .5;
float roughness = .1;
color specularcolor = 1;
string texturename = "";)
{
normal Nf = faceforward(normalize(N), I);
vector V = -normalize(I);
Oi = Os;
Ci = Os * ( Cs * color texture(texturename) *
( Ka * ambient() + Kd * diffuse(Nf))
+ specularcolor * Ks * specular(Nf, V, roughness));
}
paintedplastic シェーダも、新しい要素は texture() 関数のみなので、変更はわずかです。
tut5.l と tut5.y をそれぞれ tut6.l と tut6.y にコピーします。
tut6.l に以下を追加します。
...
"texture" { return TEXTURE; }
...
tut6.y には、TEXTURE トークン定義を追加し、 texture_type に TEXTURE を追加します。
... %token TEXTURTE ...texture_type ...
| TEXUTRE
...
コンパイル、実行
今までと同様にして、コンパイルします。
$ flex tut6.l $ bison -y -d -t tut6.y $ gcc lex.yy.c y.tab.c -lfl
実行して parse error が出なければ OK です。
まとめ
以上で、標準 RenderMan サーフェスシェーダは全部になります。とりあえず、今はサーフェスシェーダのみに限定して実装を進めていきます。
これで、とりあえず入力のシェーダの構文チェックのみができるようになりました。
入力のシェーダをいろいろカスタマイズしてみて、うまく動いているか確かめてみてください。
次回からは、ここから実際に構文ツリーを作成し、中間コードもしくはC/C++への変換コードを出力するというパーサのメイン部分(C言語で記述)の実装に進んでいきます。
前回までで、とりあえず標準 RenderMan シェーダの構文チェックまでは完成しました。
RenderMan シェーダ言語には、繰り返し(for), 分岐(if) などもっといろいろな構文がありますが、とりあえずは標準 RenderMan シェーダの C/C++ コンバータを作成することにします。
今回からは、入力のシェーダ言語の構文木(parse tree, syntax tree)を作成していきます。これは、シェーダ言語を木構造で表現します。
たとえば、 constant.sl の
Ci = Os * Cs
は、以下のように2分木で表現することができます。

四角が木の葉ノードになり、丸が木の節になります。
このようにしてシェーダ言語全体を2分木の構文木として作成します。
構文木ができたら、最後に構文木をルートから辿っていき、最終的なコードを出力します。
このとき、中間言語のコードを出力するようにもできますし、 C/C++ のコードを出力するようにもできます(今回はC/C++コードを出力)。
記号表の作成
構文木を作成するまえに、関数名や変数名を管理する記号表を作成します。記号表を作成することで、同じ変数名が二重に宣言されていないかチェックしたり、識別子の名前からその型(float, color など)やクラス(変数、関数など)を参照することができるようになります。
記号表の中身は、検索を効率的にするために、通常はハッシュテーブルで実装します。
今回はまず、 constant.sl シェーダが処理できるようにコードを書きます。そのため、管理する記号は変数のみになります。
記号表を管理するプログラムのヘッダーファイル sym.h です。
#ifndef SYM_H
#define SYM_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct _sym_t
{
char *name; /* name of IDENTIFIER */
int type; /* type of IDENTIFIER */
int class; /* class of IDENTIFIER */
struct _sym_t *next; /* next symbol */
} sym_t;
/* register variable symbol into the symbol table. */
extern sym_t *var_reg(char *name, int type);
/* lookup symbol by name. */
extern sym_t *lookup_sym(char *name);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
sym_t 構造体は、識別子の名前、識別子の型(float, color など)、識別子のクラス(変数, 関数) を保持する記号データです。
struct _sym_t *next は、記号表をハッシュで管理するために、ハッシュ値が同じであった場合にリスト構造で衝突を回避するために用います。
extern sym_t *var_reg(char *name, int type);
変数を記号表に登録し、作成された sym_t 構造体へのポインタを返します。
extern sym_t *lookup_sym(char *name);識別子の名前から、記号表を検索し、見つかった記号データを返します。
記号表を管理するプログラムの本体ファイル sym.c です。
#include <stdio.h>
#include <stdlib.h>
#include "y.tab.h"
#include "sym.h"
#define CLASS_VAR 0
#define HASHTABLE_SIZE 131
static sym_t *hashtable[HASHTABLE_SIZE];
static unsigned int hash(const char *str);
static sym_t *sym_reg(char *name, int type, int class);
sym_t *
var_reg(char *name, int type)
{
sym_t *p;
p = lookup_sym(name);
if (p == NULL) {
p = sym_reg(name, type, CLASS_VAR);
} else {
/* duplicated declaration */
return NULL;
}
return p;
}
sym_t *
lookup_sym(char *name)
{
unsigned int h;
sym_t *p;
h = hash(name);
p = hashtable[h];
for (; p != NULL && p->name != name; p = p->next);
return p;
}
/* --- private functions --- */
static unsigned int
hash(const char *str)
{
const char *p = str;
unsigned long h = *p;
if (h) {
for (p += 1; *p != '\0'; p++) {
h = (h << 5) - h + *p;
}
}
return h % HASHTABLE_SIZE;
}
static sym_t *
sym_reg(char *name, int type, int class)
{
unsigned int h;
sym_t *p;
h = hash((const char *)name);
p = (sym_t *)malloc(sizeof(sym_t));
p->name = name;
p->type = type;
p->class = class;
p->next = hashtable[h];
hashtable[h] = p;
return p;
}
識別子の型の定義には、bison が出力したファイルを利用します。
#include <y.tab.h>
#define CLASS_VAR 0
は、変数クラスを定義します。今回は識別子は変数のみですが、後でここに CLASS_FUNC などとして関数クラスなどを追加していきます。
#define HASHTABLE_SIZE 131
は、ハッシュテーブルの大きさです。ハッシュアルゴリズムでは、この値は素数にするのがよいそうです。
static sym_t *sym_reg(char *name, int type, int class);
では、実際に識別子の名前、型、クラスから記号データを作成し、ハッシュテーブル hashtable に登録します。
sym_reg() の、
p->name = name;
では、p->name が name を指すだけになっています。ここで入力の name は、bison が切り出した文字列になるのですが、 bison では、切り出した文字列は、そのメモリは解放されないようなのでこのようにしてもOKのようです(もしくは入力ファイルのメモリ位置を指しているのかも)。
hashtable は、ハッシュ値が衝突する場合を考えて、リンクリスト構造になっています。図示すると以下のようになります。
テスト
sym.c がちゃんと動作しているか、簡単なテストプログラム testsym.c を書いてみます。
#include <stdio.h>
#include <stdlib.h>
#include "y.tab.h"
#include "sym.h"
int
main(int argc, char **argv)
{
char *cs = "Cs";
char *oi = "Oi";
char *os = "Os";
char *tmp = "tmp";
char *cs2 = "Cs";
sym_t *q;
if (var_reg(cs, COLOR) == NULL) {
printf("duplicated declaration\n");
exit(-1);
}
if (var_reg(oi, COLOR) == NULL) {
printf("duplicated declaration\n");
exit(-1);
}
if (var_reg(os, COLOR) == NULL) {
printf("duplicated declaration\n");
exit(-1);
}
q = lookup_sym(cs);
if (q) {
printf("query = %s: name = %s, type = %d\n",
cs, q->name, q->type);
} else {
printf("query = %s: null\n", cs);
}
q = lookup_sym(oi);
if (q) {
printf("query = %s: name = %s, type = %d\n",
oi, q->name, q->type);
} else {
printf("query = %s: null\n", oi);
}
q = lookup_sym(os);
if (q) {
printf("query = %s: name = %s, type = %d\n",
os, q->name, q->type);
} else {
printf("query = %s: null\n", os);
}
q = lookup_sym(tmp);
if (q) {
printf("query = %s: name = %s, type = %d\n",
tmp, q->name, q->type);
} else {
printf("query = %s: not found\n", tmp);
}
if (var_reg(cs, COLOR) == NULL) {
printf("%s: duplicated declaration\n", cs);
exit(-1);
} else {
printf("???\n");
}
}
コンパイルして、実行して確かめてみます。
$ bison -y -d -t tut6.y <--- y.tab.h が生成される $ gcc sym.c testsym.c $ ./a.out query = Cs: name = Cs, type = 264 query = Oi: name = Oi, type = 264 query = Os: name = Os, type = 264 query = tmp: not found Cs: duplicated declaration
以上のようになれば成功です。
今回から、構文木のデータ構造を作成していきます。
構文木は2分木で表現できるので、通常のアルゴリズムの2分木のデータ構造で表すことができます。
構文木のコードのヘッダファイル tree.h は以下になります。
#ifndef TREE_H
#define TREE_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct _node_t
{
int opcode; /* opcode */
int type; /* type of opcode */
struct _node_t *left; /* left of children */
struct _node_t *right; /* right of children */
} node_t;
/* make leaf node */
extern node_t *make_leaf(char *name);
/* make tree node */
extern node_t *make_node(int opcode, node_t *left, node_t *right);
extern void dump_node(node_t *node);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
extern node_t *make_leaf(char *name);
make_leaf()では、識別子の葉ノードを作成します。内部では、まず name から記号表を参照し、対応する記号データを取得します。記号データはsym_t *型ですが、これをnode_t *型にキャストしてノードの left に収納します。right は NULL にします。。
extern node_t *make_node(int opcode, node_t *left, node_t *right);
make_node では、オペコード、左の子、右の子からノードを作成します。
構文木のコードの本体 tree.c は以下になります。
#include <stdio.h>
#include <stdlib.h>
#include "tree.h"
#include "sym.h"
#include "y.tab.h"
#define IDS 400
static node_t *new_node(int opcode, node_t *left, node_t *right);
static void dump_node_trav(node_t *node, int indent);
static void opstr(char *str, int opcode);
node_t *
make_leaf(char *name)
{
sym_t *sp;
node_t *np;
sp = lookup_sym(name);
if (sp == NULL) {
/* ??? */
return NULL;
}
np = new_node(IDS, (node_t *)sp, NULL);
np->type = sp->type;
return np;
}
node_t *
make_node(int opcode, node_t *left, node_t *right)
{
node_t *np;
np = new_node(opcode, left, right);
np->type = left->type;
return np;
}
void
dump_node(node_t *node)
{
printf("root = ");
dump_node_trav(node, 0);
}
/* --- private functions --- */
static node_t *
new_node(int opcode, node_t *left, node_t *right)
{
node_t *np;
np = (node_t *)malloc(sizeof(node_t));
np->opcode = opcode;
np->left = left;
np->right = right;
return np;
}
static void
dump_node_trav(node_t *node, int indent)
{
int i;
char buf[16];
opstr(buf, node->opcode);
printf("[OP] %s\n", buf);
if (node->opcode == IDS) {
for (i = 0; i < indent; i++) {
printf(" ");
}
printf(" left = %s\n", ((sym_t *)node->left)->name);
for (i = 0; i < indent; i++) {
printf(" ");
}
printf(" right = null\n");
} else {
for (i = 0; i < indent; i++) {
printf(" ");
}
printf(" left = ");
if (node->left) {
dump_node_trav(node->left, indent + 1);
} else {
printf("null\n");
}
for (i = 0; i < indent; i++) {
printf(" ");
}
printf(" right = ");
if (node->right) {
dump_node_trav(node->right, indent + 1);
} else {
printf("null\n");
}
}
}
static void
opstr(char *str, int opcode)
{
switch (opcode) {
case IDS:
strcpy(str, "ID");
break;
case OPMUL:
strcpy(str, "'*'");
break;
case OPASSIGN:
strcpy(str, "'='");
break;
default:
strcpy(str, "unknown");
break;
}
}
#define IDS 400
これは、識別子を表すオペコードです。 オペコードの定数には、bison の出力する y.tab.h も利用しています。 bison のトークン定義の定数は 257 から始まるので、これとかぶらないように 400 から数字を振ってあります。
レキサを変更して、識別子が見つかった場合はその識別子の文字列も返すように変更します。
%{
#include <string.h>
#include "tree.h"
#include "y.tab.h" /* tokens */
%}
IDENT [a-zA-Z_][a-zA-Z_0-9]*
exp [eE][-+]?[0-9]+
NUM [-+]?([0-9]+|(([0-9]+)|([0-9]+\.[0-9]*)|(\.[0-9]+)){exp}?)
STR \"([^\"\n]|\"\")*\"
%%
"float" { return FLOAT; }
"vector" { return VECTOR; }
"normal" { return NORMAL; }
"color" { return COLOR; }
"string" { return STRING; }
"environment" { return ENVIRONMENT; }
"texture" { return TEXTURE; }
"surface" { return SURFACE; }
{IDENT} { yylval.string = strdup((const char *)yytext);
return IDENTIFIER; }
{NUM} { return NUMBER; }
{STR} { return STRINGCONSTANT; }
[ \t\n]+ /* blank, tab, new line */
. { return yytext[0]; }
%%
yylval.string は、 tut7.y の方であたらしく定義してある変数です。
パーサ tut7.y は以下になります。
%{
#include <stdio.h>
#include "sym.h"
#include "tree.h"
%}
%union {
char *string;
node_t *np;
}
%token SURFACE
%token <string> IDENTIFIER
%token NUMBER
%token STRINGCONSTANT
%token FLOAT NORMAL VECTOR COLOR STRING
%token ENVIRONMENT
%token TEXTURE
%token OPASSIGN OPMUL
%right '='
%left '+'
%left '*'
%left UMINUS /* unary minus */ TYPECAST
%type <np> assignexpression expression primary
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' formals ')'
'{' statements '}'
;
shader_type : SURFACE
;
formals : /* empty */
| formal_variable_definitions
| formals ';' formal_variable_definitions
| formals ';'
;
formal_variable_definitions : typespec def_expressions
;
variable_definitions : typespec def_expressions
;
typespec : type
;
type : FLOAT
| NORMAL
| VECTOR
| COLOR
| STRING
;
def_expressions : def_expression
| def_expressions ',' def_expression
;
def_expression : IDENTIFIER def_init
{
}
;
def_init : /* empty */
| '=' expression
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : variable_definitions ';'
| assignexpression ';'
{
dump_node($1);
}
;
/* --- expressions --- */
expression : primary
{
$$ = $1;
}
| expression '+' expression
{
}
| expression '*' expression
{
$$ = make_node(OPMUL, $1, $3);
}
| '-' expression %prec UMINUS
{
}
| '(' expression ')'
{
}
| typecast expression %prec TYPECAST
{
}
;
primary : NUMBER
{
}
| STRINGCONSTANT
{
}
| IDENTIFIER
{
var_reg($1, COLOR);
$$ = make_leaf($1);
}
| texture
{
}
| procedurecall
{
}
| assignexpression
{
}
;
typecast : COLOR
;
assignexpression : IDENTIFIER '=' expression
{
var_reg($1, COLOR);
$$ = make_node(OPASSIGN,
make_leaf($1),
$3);
}
;
procedurecall : IDENTIFIER '(' proc_arguments ')'
{
}
;
proc_arguments : /* empty */
| expression
{
}
| proc_arguments ',' expression
{
}
;
texture : texture_type
'(' texture_arguments ')'
;
texture_type : ENVIRONMENT
| TEXTURE
;
texture_arguments : expression
{
}
| texture_arguments ',' expression
{
}
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
%union {
char *string;
node_t *np;
}
は、bison のルールをC言語の変数としてアクセスできるようにするためのユニオンデータです。また flex からは、 yylval.string や yylval.np としてアクセスすることもできます。
ルールがどのようなC言語の型を持つかは、 %type で指定します。
%type <np> assignexpression expression primary
assignexpression, expression, primary は npデータ、つまり node_t *型を持つように指定しています。
トークンにも型を指定することができます。
%token <string> IDENTIFIER
IDENTIFIER トークンは、char *型の変数になります。
コンパイル、実行
以下のようにコンパイルします。
$ flex tut7.l $ bison -y -d -t tut7.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl
constant.sl シェーダを渡すと、構文木のデータが以下のように出力されます。
root = [OP] '='
left = [OP] ID
left = Oi
right = null
right = [OP] ID
left = Os
right = null
root = [OP] '='
left = [OP] ID
left = Ci
right = null
right = [OP] '*'
left = [OP] ID
left = Os
right = null
right = [OP] ID
left = Cs
right = null

今回は、constant.sl を、C/C++ のコードへと出力し、簡単なレンダラと組み合わせて上の絵がでるところまでできるようにします。
RenderMan シェーディング言語はベクトル型言語なので、そのままC言語へのコードへと出力することができません。たとえば、
color dst, a, b; dst = a * b;
であれば、これをC言語で書き直すとすると、
#define vec_mul(dst, a, b) ((dst)[0] = (a)[0] * (b)[0], \
(dst)[1] = (a)[1] * (b)[1], \
(dst)[1] = (a)[1] * (b)[1])
float dst[3], a[3], b[3];
vec_mul(dst, a, b);
などとなるように出力する必要があります。ただ、このマクロ方式(もしくは関数方式)だと、入れ子になった式、
dst = a * (b + c)
は、一時変数を用いて、
float tmp[3]; vec_add(tmp, b, c); vec_mul(dst, a, tmp);
となるようなコードを出力するようにしなければいけません。
C++の演算子オーバーロードを使ってC++でコンパイルするという解決もありますが、後々のことも考えて、ちょっと出力が大変ですがCのコードを出力するようにします。
Cシェーダヘッダファイル
ベクトル演算や、シェーダ言語の組み込み関数に対応するC言語の関数などを定義するヘッダファイル shader.h を用意します。
#ifndef SHADER_H
#define SHADER_H
#ifdef __cplusplus
extern "C" {
#endif
typedef double ri_vector_t[4];
#define ri_color_t ri_vector_t
#define ri_vector_copy(dst, src) ((dst)[0] = (src)[0], \
(dst)[1] = (src)[1], \
(dst)[2] = (src)[2])
#define ri_vector_mul(dst, a, b) ((dst)[0] = (a)[0] * (b)[0], \
(dst)[1] = (a)[1] * (b)[1], \
(dst)[2] = (a)[2] * (b)[2])
/* shader output variables */
typedef struct _ri_output_t
{
ri_color_t Ci;
ri_color_t Oi;
} ri_output_t;
/* shader input variables */
typedef struct _ri_input_t
{
ri_color_t Os;
ri_color_t Cs;
} ri_input_t;
/* shader input state */
typedef struct _ri_status_t
{
ri_input_t input;
} ri_status_t;
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
ri_output_tはシェーダの出力変数(Oi, Ciなど)をあつかう構造体、ri_status_tはシェーダがレンダラのステートを参照するための構造体(今回はシェーダの入力変数のみ)です。C言語でのシェーダコードは、以下のような形式をとるようにします。
void
constant(ri_output_t *output, ri_status_t *status)
{
...
output->Ci = status->input.Cs;
}
関数名はシェーダの名前になります。レンダラ側からは、 *status に必要な情報をセットし、シェーダ関数を呼び出し、*output に出力されたデータを受け取るようにします。
今回はシェーダコードはまだ DLL 化せずに、静的リンクでレンダラに組み込むようにします。
構文木を処理するプログラムを変更します。
tree.h は以下になります。
#ifndef TREE_H
#define TREE_H
#include "sym.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct _node_t
{
int opcode; /* opcode */
int type; /* type of opcode */
sym_t *tmpvar; /* temporary variable */
struct _node_t *left; /* left of children */
struct _node_t *right; /* right of children */
} node_t;
/* make leaf node */
extern node_t *make_leaf(char *name);
/* make tree node */
extern node_t *make_node(int opcode, node_t *left, node_t *right);
extern void write_node(node_t *node);
extern void write_tmpvar(node_t *node);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
まず、 node_t に tmpvar が追加されています。
sym_t *tmpvar; /* temporary variable */
これは、ノードの一時変数になります。ノードの両方の子がどちらも葉ノードでない場合、一時変数を新しく作成してここに割り当てるようにします。一時変数は "tmp番号" という名前にします。番号は1つづつ増やしていき重ならないようにします。
たとえば、
Ci = Os * Cs;
では、右の子が "*" ノードになるので、一時変数 tmp0 を作成し、コードを出力するときは、
ri_vector_mul(tmp0, Os, Cs); ri_vector_copy(Ci, tmp0);
となるようにします。
extern void write_node(node_t *node);
では、式をトラバースしてCのコードを出力します。
extern void write_tmpvar(node_t *node);
では、式で利用される一時変数の変数宣言のCのコードを出力します。
tree.c は以下になります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sym.h"
#include "tree.h"
#include "y.tab.h"
#define IDS 400
static int ntmpvar = 0;
static node_t *new_node(int opcode, node_t *left, node_t *right);
static void typestr(char *str, int type);
static sym_t *new_tmpvar(int type);
static void write_node_trav(node_t *node);
static void write_tmpvar_trav(node_t *node);
static const char *var_prefix(const char *str);
node_t *
make_leaf(char *name)
{
sym_t *sp;
node_t *np;
sp = lookup_sym(name);
if (sp == NULL) {
/* ??? */
return NULL;
}
np = new_node(IDS, (node_t *)sp, NULL);
np->type = sp->type;
return np;
}
node_t *
make_node(int opcode, node_t *left, node_t *right)
{
node_t *np;
np = new_node(opcode, left, right);
np->type = left->type;
if (opcode != IDS && opcode != OPASSIGN) {
/* allocate temporaly variable. */
np->tmpvar = new_tmpvar(np->type);
}
return np;
}
void
write_node(node_t *node)
{
write_node_trav(node);
}
void
write_tmpvar(node_t *node)
{
write_tmpvar_trav(node);
}
/* --- private functions --- */
static node_t *
new_node(int opcode, node_t *left, node_t *right)
{
node_t *np;
np = (node_t *)malloc(sizeof(node_t));
np->opcode = opcode;
np->left = left;
np->right = right;
np->tmpvar = NULL;
return np;
}
static void
typestr(char *str, int type)
{
switch (type) {
case COLOR:
strcpy(str, "ri_color_t");
break;
default:
strcpy(str, "unknown");
break;
}
}
static sym_t *
new_tmpvar(int type)
{
char buf[256];
char *newvar;
sym_t *sp;
sprintf(buf, "tmp%d", ntmpvar); ntmpvar++;
sp = lookup_sym(buf);
while (sp) {
/* find unique name for temporaly variable. */
sprintf(buf, "tmp%d\n", ntmpvar); ntmpvar++;
sp = lookup_sym(buf);
}
newvar = strdup((const char *)&buf[0]);
sp = var_reg(newvar, type);
return sp;
}
static void
write_node_trav(node_t *node)
{
sym_t *sp;
char *dstvar, *leftvar, *rightvar;
if (node->left && node->left->opcode != IDS) {
write_node_trav(node->left);
}
if (node->right && node->right->opcode != IDS) {
write_node_trav(node->right);
}
switch (node->opcode) {
case IDS:
return;
case OPMUL:
if (!node->tmpvar) {
printf("???: !node->tmpvar\n");
exit(-1);
}
if (node->left->opcode == IDS) {
sp = (sym_t *)node->left->left;
leftvar = sp->name;
} else if (node->left->tmpvar){
leftvar = node->left->tmpvar->name;
} else {
/* ??? */
printf("???: left var\n");
exit(-1);
}
if (node->right->opcode == IDS) {
sp = (sym_t *)node->right->left;
rightvar = sp->name;
} else if (node->right->tmpvar){
rightvar = node->right->tmpvar->name;
} else {
/* ??? */
printf("???: right var\n");
exit(-1);
}
printf("\tri_vector_mul(%s%s, %s%s, %s%s);\n",
var_prefix(node->tmpvar->name), node->tmpvar->name,
var_prefix(leftvar), leftvar,
var_prefix(rightvar), rightvar);
break;
case OPASSIGN:
if (node->left->opcode == IDS) {
sp = (sym_t *)node->left->left;
leftvar = sp->name;
} else {
/* ??? */
printf("???: left var\n");
exit(-1);
}
if (node->right->opcode == IDS) {
sp = (sym_t *)node->right->left;
rightvar = sp->name;
} else if (node->right->tmpvar){
rightvar = node->right->tmpvar->name;
} else {
/* ??? */
printf("???: right var\n");
exit(-1);
}
printf("\tri_vector_copy(%s%s, %s%s);\n",
var_prefix(leftvar), leftvar,
var_prefix(rightvar), rightvar);
}
}
static void
write_tmpvar_trav(node_t *node)
{
sym_t *sp;
char vartype[256];
if (node->left && node->left->opcode != IDS) {
write_tmpvar_trav(node->left);
}
if (node->right && node->right->opcode != IDS) {
write_tmpvar_trav(node->right);
}
if (!node->tmpvar) return;
typestr(vartype, node->tmpvar->type);
printf("\t%s %s;\n", vartype, node->tmpvar->name);
}
static const char *
var_prefix(const char *str)
{
static const char *outputprefix = "output->";
static const char *inputprefix = "status->input.";
static const char *noprefix = "";
if (strcmp(str, "Oi") == 0) return outputprefix;
else if (strcmp(str, "Ci") == 0) return outputprefix;
else if (strcmp(str, "Os") == 0) return inputprefix;
else if (strcmp(str, "Cs") == 0) return inputprefix;
return noprefix;
}
レキサには変更がありません。 tut7.l を tut8.l にコピーします。
パーサ tut8.y は以下になります。
%{
#include <stdio.h>
#include "tree.h"
#define EXPRLIST_SIZE 1024
static node_t *exprlist[EXPRLIST_SIZE];
static int nexpr = 0;
void
write_header()
{
printf("#include <stdio.h>\n");
printf("#include <stdlib.h>\n");
printf("\n");
printf("#include \"shader.h\"\n");
printf("\n");
}
void
write_funcarg()
{
printf("(ri_output_t *output, ri_status_t *status)");
}
%}
%union {
char *string;
node_t *np;
}
%token SURFACE
%token <string> IDENTIFIER
%token NUMBER
%token STRINGCONSTANT
%token FLOAT NORMAL VECTOR COLOR STRING
%token ENVIRONMENT
%token TEXTURE
%token OPASSIGN OPMUL
%right '='
%left '+'
%left '*'
%left UMINUS /* unary minus */ TYPECAST
%type <np> assignexpression expression primary
%%
/* --- declarations --- */
definitions : shader_definition
;
shader_definition : shader_type IDENTIFIER '(' formals ')'
'{' statements '}'
{
int i;
write_header();
printf("void\n");
printf("%s", $2);
write_funcarg();
printf("\n{\n");
for (i = 0; i < nexpr; i++) {
write_tmpvar(exprlist[i]);
}
printf("\n");
for (i = 0; i < nexpr; i++) {
write_node(exprlist[i]);
}
printf("}\n");
}
;
shader_type : SURFACE
;
formals : /* empty */
| formal_variable_definitions
| formals ';' formal_variable_definitions
| formals ';'
;
formal_variable_definitions : typespec def_expressions
;
variable_definitions : typespec def_expressions
;
typespec : type
;
type : FLOAT
| NORMAL
| VECTOR
| COLOR
| STRING
;
def_expressions : def_expression
| def_expressions ',' def_expression
;
def_expression : IDENTIFIER def_init
{
}
;
def_init : /* empty */
| '=' expression
;
/* --- statements --- */
statements : /* empty */
| statements statement
;
statement : variable_definitions ';'
| assignexpression ';'
{
exprlist[nexpr] = $1;
nexpr++;
}
;
/* --- expressions --- */
expression : primary
{
$$ = $1;
}
| expression '+' expression
{
}
| expression '*' expression
{
$$ = make_node(OPMUL, $1, $3);
}
| '-' expression %prec UMINUS
{
}
| '(' expression ')'
{
}
| typecast expression %prec TYPECAST
{
}
;
primary : NUMBER
{
}
| STRINGCONSTANT
{
}
| IDENTIFIER
{
var_reg($1, COLOR);
$$ = make_leaf($1);
}
| texture
{
}
| procedurecall
{
}
| assignexpression
{
}
;
typecast : COLOR
;
assignexpression : IDENTIFIER '=' expression
{
var_reg($1, COLOR);
$$ = make_node(OPASSIGN,
make_leaf($1),
$3);
}
;
procedurecall : IDENTIFIER '(' proc_arguments ')'
{
}
;
proc_arguments : /* empty */
| expression
{
}
| proc_arguments ',' expression
{
}
;
texture : texture_type
'(' texture_arguments ')'
;
texture_type : ENVIRONMENT
| TEXTURE
;
texture_arguments : expression
{
}
| texture_arguments ',' expression
{
}
;
%%
int
main(int argc, char **argv)
{
extern FILE *yyin;
extern int yydebug;
if (argc < 2) {
printf("usage: %s file.sl\n", argv[0]);
exit(-1);
}
yyin = fopen(argv[1], "r");
//yydebug = 1;
yyparse();
return 0;
}
yyerror(char *s)
{
printf("%s\n", s);
}
C言語では、ステートメント途中で変数を宣言することができないで、一度式は全部 exprlist[] に保存しておき、まず一時変数の変数宣言のコードを書き出し、次に式のコードを出力するようにしています。
コンパイル、実行
$ flex tut8.l $ bison -y -d -t tut8.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl
constant.sl シェーダを渡すと、以下のようなCのコードが出力されます。
$ ./a.out constant.sl
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
void
constant(ri_output_t *output, ri_status_t *status)
{
ri_color_t tmp0;
ri_vector_copy(output->Oi, status->input.Os);
ri_vector_mul(tmp0, status->input.Os, status->input.Cs);
ri_vector_copy(output->Ci, tmp0);
}
これを constant.c としてファイルに保存しておきます。
$ ./a.out constant.sl > constant.c
テストレンダラ
シェーダ関数を呼ぶ簡単なレンダラ render.c を書きます。いくつかの情報は決めうちにします。
#include <stdio.h>
#include "shader.h"
extern void constant(ri_output_t *output, ri_status_t *status);
static double sphere_position[3] = {0.0, 0.0, -2.0};
static double sphere_radius = 0.5;
static int clamp(double d);
static int intersect(double ray_origin[3], double ray_direction[3]);
static void exec_shader(double outcol[3], double normal[3]);
static int
clamp(double d)
{
int i;
i = (int)(d * 255.0);
if (i < 0) i = 0;
if (i > 255) i = 255;
return i;
}
static int
intersect(double ray_origin[3],
double ray_direction[3] /* unit vector */ )
{
double oc[3]; /* the vector from sphere center to ray origin */
double b, c, d;
double sr2;
oc[0] = ray_origin[0] - sphere_position[0]; /* x */
oc[1] = ray_origin[1] - sphere_position[1]; /* y */
oc[2] = ray_origin[2] - sphere_position[2]; /* z */
b = 2.0 * (ray_direction[0] * oc[0] +
ray_direction[1] * oc[1] +
ray_direction[2] * oc[2]);
sr2 = sphere_radius * sphere_radius;
c = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - sr2;
d = (b * b - 4.0 * c);
if (d > 0.0) return 1; /* hit */
return 0; /* no hit */
}
static void
exec_shader(double outcol[3], double normal[3])
{
ri_output_t out;
ri_status_t status;
status.input.Cs[0] = 1.0;
status.input.Cs[1] = 0.0;
status.input.Cs[2] = 0.0;
status.input.Os[0] = 1.0;
status.input.Os[1] = 1.0;
status.input.Os[2] = 1.0;
constant(&out, &status);
outcol[0] = out.Ci[0];
outcol[1] = out.Ci[1];
outcol[2] = out.Ci[2];
}
int
main(int argc, char **argv)
{
FILE *fp;
int i, j, k, l;
int width = 256;
int height = 256;
int xsamples = 2, ysamples = 2;
double sx, sy;
double hw, hh;
double org[3], dir[3];
double outcol[3];
double accumcol[3];
const char filename[] = "image.ppm";
fp = fopen(filename, "w");
if (fp == NULL) {
printf("Cannot create file : %s\n", filename);
exit(-1);
}
hw = (double)width / 2.0;
hh = (double)height / 2.0;
dir[0] = 0.0;
dir[1] = 0.0;
dir[2] = -1.0;
fprintf(fp, "P3\n"); /* magic number */
fprintf(fp, "# tutorial1_2\n"); /* comment */
fprintf(fp, "%d %d\n", width, height);
fprintf(fp, "255\n"); /* max pixel value */
for (j = 0; j < height; j++) {
for (i = 0; i < width; i++) {
accumcol[0] = 0.0;
accumcol[1] = 0.0;
accumcol[2] = 0.0;
for (l = 0; l < ysamples; l++) {
for (k = 0; k < xsamples; k++) {
sx = ((double)i +
(double)k / xsamples - hw) / hw;
sy = ((double)j +
(double)l / ysamples - hh) / hh;
org[0] = sx;
org[1] = sy;
org[2] = 0.0;
if (intersect(org, dir)) {
exec_shader(outcol, dir);
accumcol[0] += outcol[0];
accumcol[1] += outcol[1];
accumcol[2] += outcol[2];
}
}
}
accumcol[0] /= (double)(xsamples * ysamples);
accumcol[1] /= (double)(xsamples * ysamples);
accumcol[2] /= (double)(xsamples * ysamples);
fprintf(fp, "%d %d %d ",
clamp(accumcol[0]),
clamp(accumcol[1]),
clamp(accumcol[1]));
}
}
fclose(fp);
}
このレンダラでは、単純な球をレイトレーシングして画像を生成します。ウィンドウシステム非依存かつ外部の画像ライブラリも使用しなくて済むように、画像には PPM というテキスト形式の単純な画像フォーマットを用いました。この PPM 形式は、 Windows であれば IfranView、その他の OS であれば gimp などで見ることができます。
もしくは、以下の PPM 画像を表示する OpenGL プログラム(glut利用) ppmview.c を使用することもできます。
#include <GLUT/glut.h>
#include <stdio.h>
#include <stdlib.h>
static int width, height;
static GLubyte *image;
void
display()
{
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glRasterPos2i(0.0, 0.0);
glDrawPixels(width, height, GL_RGB, GL_UNSIGNED_BYTE, image);
}
void
key(unsigned char key, int x, int y)
{
if (key == 27) exit(0); /* ESC key */
}
void
reshape(int w, int h)
{
glViewport(0, 0, (GLint)w, (GLint)h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 1.0, 0.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
int
main(int argc, char **argv)
{
FILE *fp;
char buf;
int i, j;
int maxval;
int r, g, b;
if (argc < 2) {
printf("Usage: %s filename.ppm\n", argv[0]);
exit(-1);
}
fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("Can't open file: %s\n", argv[1]);
exit(-1);
}
/* file has a correct magic number? */
if (fgetc(fp) != 'P' || fgetc(fp) != '3' || fgetc(fp) != '\n') {
printf("File is not a ppm format.\n");
exit(-1);
}
/* skip comments. */
while((buf = fgetc(fp)) == '#') {
while(fgetc(fp) != '\n'); /* skip until newline. */
}
ungetc(buf, fp);
/* get image width and height. */
fscanf(fp, "%d %d\n", &width, &height);
/* get max value. */
fscanf(fp, "%d\n", &maxval);
image = (GLubyte *)malloc(width * height * 3 * sizeof(GLubyte));
for (j = height - 1; j >= 0; j--) {
for (i = 0; i < width; i++) {
fscanf(fp, "%d %d %d ", &r, &g, &b);
image[(i + j * width) * 3 ] = (GLubyte)r;
image[(i + j * width) * 3 + 1] = (GLubyte)g;
image[(i + j * width) * 3 + 2] = (GLubyte)b;
}
}
glutInit(&argc, argv);
glutInitWindowPosition(0, 0);
glutInitWindowSize(width, height);
glutCreateWindow(argv[0]);
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
glutKeyboardFunc(key);
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutMainLoop();
}
レンダラのコンパイル、実行
生成されたシェーダコードとレンダラを一緒にコンパイルし、実行します。
$ gcc constant.c render.c $ ./a.out
カレントディレクトリに image.ppm というレンダリング画像が生成されます。赤い円が表示されれば成功です。
そろそろコードが長くなってきたので、次回からはソースコードは掲載せずにファイルへのリンクにしたいと思います。

続いて、 matte.sl を Cのコードへと出力できるようにします。
今回は、
o シェーダパラメータ
o 関数呼び出し
の実装が大きな変更点です。
まず、シェーダパラメータですが、今回はハッシュテーブルでの解決にすることにしました。その詳細の前に、メンタルレイでのシェーダパラメータの処理について少し。
メンタルレイでのシェーダパラメータ
メンタルレイの場合ですと、シェーダパラメータは構造体渡しになります。
struct phong {
miColor ambient;
miColor diffuse;
};
miBoolean phong(
miColor *result,
miState *state,
struct phong *paras)
{
miColor *diffuse = mi_eval_color(¶s->diffuse)
...
}
シェーダパラメータを任意の順でシーン記述ファイルで指定できるように、メタデータとしてシーン記述ファイル内にシェーダ宣言を記述します。このシェーダ宣言は、構造体の順番と一致していなければなりません。
declare shader
color "phong" (color "ambient",
color "diffuse")
version 1
end declare
こうすることで、シェーダを利用するときには、シェーダパラメータは任意の順(もしくは省略)で指定することができます。
...
shader "mymaterial" "phong" (
"diffuse" 0.5 0.5 0.5
"ambient" 0.2 0.2 0.2)
...
今回のシェーダパラメータの実装
今回は、変数の順番を気にしなくて済むように、ハッシュテーブル渡しでシェーダパラメータの解決をすることにします。ただ、メンタルレイのような構造体渡しも、Cのシェーダコードの他にシェーダパラメータのメタデータを出力することで実現できなくもないとは思います。
matte.sl のシェーダパラメータ
surface matte(float Ka = 1.0; float Kd = 1.0;) ...
は、以下のようなCコードに変換します。
void
matte(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
ri_vector_t Ka;
ri_vector_t Kd;
ri_param_eval(Ka, param, "Ka");
ri_param_eval(Ka, param, "Ka");
}
ri_parameter_t はシェーダパラメータを保持しておくハッシュテーブル、 ri_param_eval() は変数名をキーとしてハッシュテーブルから値を取り出す関数になります。
float などのスカラ型は、すべてベクトル型に変換するようにします。
RenderMan ではシェーダパラメータにはデフォルト値を与える必要があるので、シェーダパラメータにデフォルト値をセットするなんからの方法が必要になります。そこで、Cのシェーダの関数とは別に、シェーダパラメータにデフォルト値をセットする関数を別に用意します。この関数はハッシュテーブルの初期化も担当するようにします。もし変数にデフォルト値が無い場合はその変数の型に合った初期値(ベクトルであれば 0.0, 0.0, 0.0, 1.0など)を割り当てるようにします。この関数は "シェーダ名_initparam()" とします。
void
matte_initparam(ri_parameter_t *param)
{
ri_vector_t tmp0;
ri_vector_t Ka;
ri_vector_t tmp1;
ri_vector_t Kd;
ri_vector_set(tmp0, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Ka, tmp0);
ri_vector_set(tmp1, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Kd, tmp1);
ri_param_add(param, "Ka", TYPEVECTOR, Ka);
ri_param_add(param, "Kd", TYPEVECTOR, Kd);
}
ri_param_add() では、ハッシュテーブルに変数を登録します。変数の型は shader.h で定義しておきます(ベクトル値(カラー値)は TYPEVECTOR)。tmp0 などの無駄な一時変数があるのは、構文上その方が出力が容易なためです。
レンダラ側から見れば、シェーダを呼ぶ時は、
o matte_initparam() を呼び出して変数の登録、初期化
o レンダラ側で、 RIB の Surface コマンドに変数があるときにはその値で更新
o matte() 本体を呼び出す。
という手順を取ります。
シェーダ関数呼び出し
シェーダの組み込み関数は、対応するCでの関数を用意します。たとえば、
color ambient();
は、
void ambient(ri_vector_t dst);
となります。
ソースコード
tut9.l
tut9.y
sym.c
sym.h
tree.c
tree.h
shader.c
shader.h
render.c
コンパイル、実行
まずシェーダコンパイラをコンパイルし、matte.sl の Cコードを作成します。
$ flex tut9.l $ bison -y -d -t tut9.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out matte.sl > matte.c
matte.c は以下のようなソースになります。
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
void
matte_initparam(ri_parameter_t *param)
{
ri_vector_t tmp0;
ri_vector_t Ka;
ri_vector_t tmp1;
ri_vector_t Kd;
ri_vector_set(tmp0, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Ka, tmp0);
ri_vector_set(tmp1, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Kd, tmp1);
ri_param_add(param, "Ka", TYPEVECTOR, Ka);
ri_param_add(param, "Kd", TYPEVECTOR, Kd);
}
void
matte(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
ri_vector_t Ka;
ri_vector_t Kd;
ri_color_t tmp2;
ri_color_t tmp3;
ri_vector_t Nf;
ri_color_t tmp4;
ri_color_t tmp5;
ri_vector_t tmp6;
ri_color_t tmp7;
ri_vector_t tmp8;
ri_vector_t tmp9;
ri_color_t tmp10;
ri_param_eval(Ka, param, "Ka");
ri_param_eval(Kd, param, "Kd");
normalize(tmp2, status->input.N);
faceforward(tmp3, tmp2, status->input.I);
ri_vector_copy(Nf, tmp3);
ri_vector_copy(output->Oi, status->input.Os);
ri_vector_mul(tmp4, status->input.Os, status->input.Cs);
ambient(tmp5);
ri_vector_mul(tmp6, Ka, tmp5);
diffuse(tmp7, Nf);
ri_vector_mul(tmp8, Kd, tmp7);
ri_vector_add(tmp9, tmp6, tmp8);
ri_vector_mul(tmp10, tmp4, tmp9);
ri_vector_copy(output->Ci, tmp10);
}
結構冗長ですが、自前で最適なコードを出力するようにするよりは、Cコンパイラ側に最適化を任せた方が遥かによいということで。
生成されたシェーダソースとレンダラを組み合わせます。
$ gcc matte.c shader.c render.c $ ./a.out
最初に示した画像が生成されるのを確認してください。グラデーションが不自然に見えるかもしれませんが、これはガンマ補正を行っていないためです。

続いて、metal.sl シェーダを Cのコードへと出力できるようにします。
今回は、単項負符号と specular() 関数の実装の追加だけになります。
単項負符号の式には、OPNEG というオペコードを持つノードを作成するようにします。
expression ...
| '-' expression %prec UMINUS
{
$$ = make_node(OPNEG, $2, NULL);
}
...
RenderMan の specular() 関数、
color specular(normal N; vector V; float roughness)
には、以下の対応するC関数を用意します。
void specular(ri_color_t dst,
ri_vector_t N, ri_vector_t V, ri_vector_t roughness)
roughness がベクトル型になっています。Cのコードではスカラ値はすべてベクトル値に変換するようにしているのでこのような定義になります。実際にspecular()内部で使用するときにはそのx要素のみを利用するようにします。
ソースコード
tar.gz 形式で一式収録するようにしました。
コンパイル、実行
いつもと同じようにシェーダコンパイラを作成し、metal.sl のCコードを出力します。
$ flex tut10.l $ bison -y -d -t tut10.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out metal.sl > metal.c
レンダラと組み合わせて、上記の画像のようになることを確認してください。
$ gcc metal.c shader.c render.c $ ./a.out

続いて、 shinymetal.sl のCコードが生成できるようにします。
今回は、
o 文字列
o テクスチャ関数
の扱いが大きな変更点になります。
文字列
まず、文字列定数に対しては、文字列定数のノードを作成する関数、
node_t *make_conststr(char *string);
を tree.c に作成します。
ri_param_add() でシェーダパラメータを追加するとき、シェーダパラメータが文字列の場合(TYPESTRINGを新しく定義)は、文字列の長さ分のメモリを割り当てて内容をコピーするようにします。
ri_param_add(...)
{
...
if (type == TYPESTRING) {
p->size = typesize[type] * (strlen((char *)val) + 1);
p->val = malloc(p->size);
memcpy(p->val, val, p->size);
} else {
...
}
typesize[type] は文字列型の大きさ(バイト数)で、これは1になります。
ri_param_eval() でシェーダパラメータを取り出すときは、文字列へのポインタを返すようにします。
ri_param_eval()
{
...
if (p->type == TYPESTRING) {
(char *)data = (char *)p->val;
} else {
memcpy(data, p->val, p->size);
}
...
}
テクスチャ関数
今回、テクスチャ関数 environment() は、単純なチェッカーボード模様を使うようにしました。
その他
shinymetal.sl には座標変換関数 vtransform() がありますが、Cの関数では今回は座標変換は行わずに、単純に入力の座標を出力にコピーするようにしています。
vtransform(ri_vector_t dst, char *from, char *to, ri_vector_t src)
{
ri_vector_copy(dst, src);
}
ソースコード
コンパイル、実行
いつものようにシェーダコンパイラをビルドして、Cのシェーダコードを出力します。
$ flex tut11.l
$ bison -y -d -t tut11.y
$ gcc lex.yy.c y.tab.c sym.c tree.c -lfl
$ ./a.out shinymetal.sl > shinymetal.c
shinymetal.c は以下のようになります。
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
void
shinymetal_initparam(ri_parameter_t *param)
{
ri_vector_t tmp0;
ri_vector_t Ka;
ri_vector_t tmp1;
ri_vector_t Ks;
ri_vector_t tmp2;
ri_vector_t Kr;
ri_vector_t tmp3;
ri_vector_t roughness;
char * tmp4;
char * texturename;
ri_vector_set(tmp0, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Ka, tmp0);
ri_vector_set(tmp1, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Ks, tmp1);
ri_vector_set(tmp2, 1.000000, 1.000000, 1.000000, 1.000000);
ri_vector_copy(Kr, tmp2);
ri_vector_set(tmp3, 0.100000, 0.100000, 0.100000, 0.100000);
ri_vector_copy(roughness, tmp3);
tmp4 = "";
texturename = tmp4;
ri_param_add(param, "Ka", TYPEVECTOR, Ka);
ri_param_add(param, "Ks", TYPEVECTOR, Ks);
ri_param_add(param, "Kr", TYPEVECTOR, Kr);
ri_param_add(param, "roughness", TYPEVECTOR, roughness);
ri_param_add(param, "texturename", TYPESTRING, texturename);
}
void
shinymetal(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
ri_vector_t Ka;
ri_vector_t Ks;
ri_vector_t Kr;
ri_vector_t roughness;
char * texturename;
ri_color_t tmp5;
ri_color_t tmp6;
ri_vector_t Nf;
ri_color_t tmp7;
ri_color_t tmp8;
ri_vector_t V;
ri_color_t tmp9;
ri_color_t tmp10;
ri_vector_t D;
char * tmp11;
char * tmp12;
ri_color_t tmp13;
ri_vector_t tmp14;
ri_color_t tmp15;
ri_vector_t tmp16;
ri_color_t tmp17;
ri_vector_t tmp18;
ri_vector_t tmp19;
ri_color_t tmp20;
ri_vector_t tmp21;
ri_vector_t tmp22;
ri_vector_t tmp23;
ri_param_eval(Ka, param, "Ka");
ri_param_eval(Ks, param, "Ks");
ri_param_eval(Kr, param, "Kr");
ri_param_eval(roughness, param, "roughness");
ri_param_eval(texturename, param, "texturename");
normalize(tmp5, status->input.N);
faceforward(tmp6, tmp5, status->input.I);
ri_vector_copy(Nf, tmp6);
normalize(tmp7, status->input.I);
ri_vector_copy(tmp8, tmp7);
ri_vector_neg(tmp8);
ri_vector_copy(V, tmp8);
normalize(tmp9, Nf);
reflect(tmp10, status->input.I, tmp9);
ri_vector_copy(D, tmp10);
tmp11 = "current";
tmp12 = "world";
vtransform(tmp13, tmp11, tmp12, D);
ri_vector_copy(D, tmp13);
ri_vector_copy(output->Oi, status->input.Os);
ri_vector_mul(tmp14, status->input.Os, status->input.Cs);
ambient(tmp15);
ri_vector_mul(tmp16, Ka, tmp15);
specular(tmp17, Nf, V, roughness);
ri_vector_mul(tmp18, Ks, tmp17);
ri_vector_add(tmp19, tmp16, tmp18);
environment(tmp20, texturename, D);
ri_vector_mul(tmp21, Kr, tmp20);
ri_vector_add(tmp22, tmp19, tmp21);
ri_vector_mul(tmp23, tmp14, tmp22);
ri_vector_copy(output->Ci, tmp23);
}
レンダラと組み合わせて、上記の画像が生成されるのを確認してください。
$ gcc shinymetal.c shader.c render.c $ ./a.out

今回は plastic.sl をCのコードへと出力できるようにします。
変更はわずかに1か所のみです。
tut12.y の write_paraminitializer() で カラー型の変数の場合も TYPEVECTOR を出力するように対応するようにするだけです。
write_paraminitializer(char *name)
{
...
switch(sp->type) {
case FLOAT:
case VECTOR:
case COLOR:
strcpy(buf, "TYPEVECTOR");
if (!formalexprlist[i]->right) {
/* set default value. */
printf("\tri_vector_set(%s", sp->name);
printf(", 0.0, 0.0, 0.0, 1.0);\n");
}
break;
...
ソースコード
コンパイル、実行
シェーダコンパイラをビルドし、plastic.sl を Cのコードへと変換します。
$ flex tut12.l
$ bison -y -d -t tut12.y
$ gcc lex.yy.c y.tab.c sym.c tree.c -lfl
$ ./a.out plastic.sl > plastic.c
レンダラと組み合わせて、上記の画像が生成されるのを確認してください。
$ gcc plastic.c shader.c render.c $ ./a.out

最後の標準 RenderMan サーフェスシェーダである paintedplastic.sl をCのコードへと変換できるようにします。
構文木のほうに変更はありません。
RenderMan の texture() 関数では、UV座標を省略してコールすることができます。
そのためCの実装の方では、UV座標はグローバル変数(tex_coords) を render.c から参照することにします。
今回は球のジオメトリしかないので、UV座標はレイとの交点の法線から計算するようにしています。
exec_shader(double outcol[3], double normal[3])
{
...
ri_vector_t nf;
double m;
...
ri_vector_set(nf, normal[0], normal[1], normal[2], 1.0);
ri_vector_normalize(nf);
m = sqrt(nf[0] * nf[0] +
nf[1] * nf[1] +
(1.0 - nf[2]) * (1.0 - nf[2])); /* because normal is
* right handed */
if (m != 0.0) {
tex_coords[0] = nf[0] / m + 0.5;
tex_coords[1] = nf[1] / m + 0.5;
} else {
tex_coords[0] = 0.5;
tex_coords[1] = 0.5;
}
...
ソースコード
コンパイル、実行
シェーダコンパイラをビルドし、Cのシェーダコードを出力します。
$ flex tut13.l $ bison -y -d -t tut13.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out paintedplastic.sl > paintedplastic.c
レンダラと組み合わせて、上記の画像が生成されることを確認してください。
$ gcc paintedplastic.c shader.c render.c (OSによっては -lm を追加) $ ./a.out
標準 RenderMan サーフェスシェーダがひと通り対応できるようになったので、ここで静的リンクから DSO(ダイナミックシェアードオブジェクト)リンクにできるようにします。
つまりはシェーダコードのプラグイン化です。ダイナミックリンク(共有ライブラリ)の仕組みはOSにより異なり、すべてを網羅するのは無理なので、ここでは OS-X(darwin), linux(FreeBSDも含む), Windows(cygwin) の3つのプラットフォームについて対応するようにします。
コンパイラは gcc、バージョンは 3.x 以降を仮定しています。
ソースコード
今回使用するソースコードです。
OS共通
まず、Windowsの場合のみ、DSOとする関数には __declspec(dllexport) を付加する必要があります。そこで、マクロ DLLEXPORT を定義します。
#ifdef WIN32 #ifdef __cplusplus #define DLLEXPORT extern "C" __declspec(dllexport) #else #define DLLEXPORT __declspec(dllexport) #endif #else #define DLLEXPORT #endif
シェーダコンパイラの方では、出力する関数の前には DLLEXPORT を付加するように変更しておきます。
続いて、現状では shader.c の一部の変数は render.c を参照しているので、 shader.c で全てが完結するようにします。
新しい関数、
void ri_status_set(ri_status_t *status);
を shader.c に導入し、render.c からは、シェーディングに必要な情報は status にセットして shader.c に渡しておくようにします。
また、実行時にDSOをロードする関数 dlload.c を用意します。
dlload.h
#ifndef DLLOAD_H
#define DLLOAD_H
#if defined(__APPLE__) && defined(__MACH__)
#include <mach-o/dyld.h>
#elif defined(WIN32)
#include <windows.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef struct _dl_module_t
{
#if defined(__APPLE__) && defined(__MACH__)
NSModule module;
#elif defined(WIN32)
HINSTANCE module;
#elif defined(LINUX)
void *module;
#else
int module;
#endif
} dl_module_t;
extern int dlload(dl_module_t *module, const char *filename);
extern void *dlgetfunc(dl_module_t *module, const char *funcname);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
dl_module_t はモジュール(DSOファイル)のハンドルです。
int dlload(dl_module_t *module, const char *filename);
では、DSOのファイル名からDSOをロードし、そのハンドルを module に返します。
void *dlgetfunc(dl_module_t *module, const char *funcname);
では、ロードしたDSOモジュールから、funcname と同じ名前の関数を探し、その関数へのアドレスを返します。
レンダラ側(render.c)では、シェーダ呼び出しの関数は関数ポインタにします。
...
typedef void (*shader_initparamproc)(ri_parameter_t *param);
typedef void (*shaderproc)(ri_output_t *output,
ri_status_t *status,
ri_parameter_t *param);
static shader_initparamproc shader_initparam;
static shaderproc shader;
...
shader(&out, &status, ¶m);
...
どのシェーダを呼ぶかは、コマンドラインの引数で指定します。
...
#ifdef WIN32
sprintf(buf, "%s.dll", argv[1]);
#else
sprintf(buf, "%s.so", argv[1]);
#endif
if (dlload(&module, buf) == 0) {
exit(-1);
}
/* initialize shader */
shader = (shaderproc)dlgetfunc(&module, argv[1]);
if (!shader) {
printf("can't get proc: %s\n", argv[1]);
exit(-1);
}
...
読み込むDSOファイルは、"シェーダ名.dll"(windowsの場合)、もしくは "シェーダ名.so"(linux, OS-Xの場合)にします。
例えば、 paintedplastic シェーダであれば、DSOのファイル名は
paintedplastic.dll(windowsの場合)
paintedplastic.so(linux, OS-Xの場合)
になり、このDSOファイル内に、paintedplastic() というシェーダ本体の関数が定義されていることにします。
以降は、各プラットフォームでの DSO 形式のシェーダの生成方法について述べます
OS-Xの場合
まず、CシェーダコードのDSOを作成します。
OS-X(darwin)では、ダイナミックリンクの形式には2種類あるようです。
一つは dyld 形式のダイナミックリンクライブラリで、これは linux や windows 使われている通常のダイナミックリンクライブラリのように、アプリケーションのビルド時(コンパイル時)にリンクして使用するのが主なもの。
もう一つはバンドル(bundle)と呼ばれ、アプリケーションが実行時にリンクするプラグインとして用いるものが主のものです。
今回はシェーダコードはプラグイン的な形式を取るので、バンドル形式でシェーダコードのDSOを作成することにします(シェーダはpaintedplasticを例としています)。
$ gcc -bundle -flat_namespace -undefined suppress \ -o paintedplastic.so paintedplastic.c
今回はDSOの拡張子を linux と同じ .so にしました。 paintedplastic.c は shader.c のシンボルに依存しているのですが、ここらへんは -undefined suppress を利用するとで後の実行時にダイナミックリンカが面倒を見てくれるようです。結構優秀ですね。
レンダラは普通にコンパイルします。
$ gcc render.c dlload.c shader.c
以下のようにシェーダの名前を引数にして実行します。
$ ./a.out paintedplastic
後は、各種シェーダコードを同様に DSO 形式にコンパイルすることで、動的にシェーダを切り替えてレンダリング処理をすることができます。
linux の場合
シェーダコードの DSO を作成します。
$ gcc -DLINUX -fPIC -c paintedplastic.c $ ld -export-dynamic -shared -o paintedplastic.so paintedplastic.o
レンダラをコンパイルする前に、 shader.c を共有ライブラリにします。
$ gcc -DLINUX -fPIC -c shader.c $ ld -export-dynamic -shared -o shader.so shader.o
レンダラをコンパイルします。
$ gcc -DLINUX render.c dlload.c shader.so -lm -ldl
以下のようにシェーダの名前を引数にして実行します。
./a.out paintedplastic
Windows(cygwin)の場合
Windows の共有ライブラリは結構制限がきつく、共有ライブラリ(DLL)を作成するときにはすべてのシンボルが解決されている必要があります。
おのおののシェーダコードは shader.c のシンボルに依存するので、まず、 shader.c の DLL を作成し、またそのスタブライブラリ(インポートライブラリ) libshader.a も出力するようにします。
$ gcc -DWIN32 -shared -o shader.dll shader.c -Wl,--out-implib=libshader.a
DSO シェーダのコンパイル時には、この生成された libshader.a とリンクさせます。
$ gcc -DWIN32 -shared -o paintedplastic.dll paintedplastic.c -L./ -lshader
レンダラも libshader.a とリンクさせてコンパイルします。
$ gcc -DWIN32 dlload.c render.c -L./ -lshader
以下のようにシェーダの名前を引数にして実行します。
./a.out paintedplastic
RenderMan シェーダ言語には、組み込み関数として noise() 関数があります。
このノイズ関数は、言ってみれば rand() などの一様乱数関数なのですが、コンピュータグラフィックスの世界では、このノイズ関数には、 まず間違いなく Ken Perlin 氏によるノイズ関数が利用されています。
An Image Synthesizer,
Ken Perlin, Computer Graphics, Vol. 19, No. 3. (also in Computer Graphics: Image Synthesis, IEEE, Salem, 1988)
このノイズ関数のソースコードは、氏のホームページで公開されています。
http://mrl.nyu.edu/~perlin/doc/oscar.html
Ken Perlin 氏によるノイズ関数、 perlin noise と呼ばれる、は、一様乱数に似ていますが、いくつかコンピュータグラフィックスの利用に便利になるような性質を持っています。
この perlin noise は、非整数ブラウン運動(fractional Brownian motion, fBm. 最初の単語はフラクショナルであってフラクタルでないことに注意!)による雲や木目などの自然物のテクスチャパターン生成に使用するのが主な利用用途になります。
RenderMan の仕様書には、 "noise() はパーリンノイズである"、とは書かれていませんが、たぶんすべての RenderMan レンダラでは noise() はパーリンノイズになっています。
松本眞氏による Mersenne Twister が、最近ではほとんどのベンダのC言語やフォートランの標準ライブラリの乱数関数の実装に用いられているようなものですね。
Perlin 氏は、このパーリンノイズに対してアカデミー賞を受賞しています。
改善されたパーリンノイズ
上記で少し述べましたが、パーリンノイズには、格子状のアーティファクトが現れるという問題があります。SIGGRAPH 2002 の論文では、そのアーティファクトを無くすパーリンノイズの改良版が発表されました。
Improving Noise
Ken Perlin, SIGGRAPH 2002, 2002
基本的にはノイズ生成時の補完をいままでのよりも高次元で行うとのことです。
ペーパーはわずか2枚ですが、それよりも驚いたのは SIGGRAPH での Ken Perlin 氏の論文発表でした。
なんかラップをいきなり歌い出してイントロ説明をしてましたから...
(英語が全然わからなかったのですが、最後にはラップ調で noise! って言っていた)
さすがメリケン国は違うなぁ...

今回からは、制御文(for, if, while)の実装に移ります。
まず最初は、 for 文の実装です。
for 文の例として、「実践CGへの誘い」(RenderMan Companion) の clouds.sl シェーダを処理できるようにします。
surface
clouds(float Kd = .8,
Ka = .2)
{
float sum;
float i, freq;
color white = color(1.0, 1.0, 1.0);
sum = 0;
freq = 4.0;
for (i = 0; i < 6; i = i + 1) {
sum = sum + 1/freq * abs(.5 - noise(freq * P));
freq = 2 * freq;
}
Ci = mix(Cs, white, sum * 4.0);
Oi = 1.0;
}
今回は変更点および追加点がいくつかあります。
o なるべくスカラ式を出力するように変更
o for 文の実装
o color 型の初期化子を追加
o 減算子と除算子の追加
o mix(), noise(), abs() 組み込み関数の追加
スカラ式の出力
いままではスカラ式はすべてベクトル型に変換してきましたが、 for 文の条件式などはスカラ式でないとやりずらかったりするので、式が全部スカラ型であった場合はスカラ式を出力するようにします。
また noise() のようにベクトル式を引数にとるが返り値がスカラ値になるような場合は、ベクトル式の部分のみベクトル式を出力するようにします。たとえば、
sum = sum + 1/freq * abs(.5 - noise(freq * P));
は、
ri_vector_set(tmp6, freq, freq, freq, 1.0); ri_vector_mul(tmp7, tmp6, status->input.P); sum = sum + 1.000000 / freq * fabs( 0.500000 - noise3d(tmp7) ) ;
のように出力するようにします。
スカラ式を処理する関数、
extern void write_scalar_node(node_t *node);
を追加します。この関数は write_node() 関数に似ていますが、与えられたノードをスカラ式で出力します。
シェーダパラメータのやりとりにも変更を加えます。
ri_param_add()
ri_param_eval()
では、スカラ値(float型)も取り扱えるように、TYPEFLOAT を追加します。スカラ値をやり取りするときは以下のようになります。
double Kd; Kd = 0.80000; ri_param_add(param, "Kd", TYPEFLOAT, &Kd); ... ri_param_eval(&Kd, param, "Kd");
for 文
以下のように for 文があるとき、
for (init; cond; step)
この構文木を以下のように作成します。

(ここらへんは、3分木でより効率的に表現するような手法もあります)
for 文のノードを作成する make_for を用意します。
node_t *
make_for(node_t *init, node_t *cond, node_t *step)
{
node_t *np1;
node_t *np2;
np2 = make_node(OPFORARG, cond, step);
np1 = make_node(OPFOR, init, np2);
return np1;
}
また、for 文が囲む式のブロックを記録するようにします。
typedef struct _exprblock_t
{
node_t *expr;
int start; /* block start */
int end; /* block end */
int indent;
} exprblock_t;
start にはブロック内の最初の式の番号を、end にはブロック内の最後の式の番号を記録します。
パーサで for 文を処理するルールは以下のようになります。
statement_list : statement
| statement_list statement
statement : variable_definitions ';'
| assignexpression ';'
{
exprlist[nexpr].expr = $1;
nexpr++;
}
| '{' statement_list '}'
| loop_control statement
{
currblockdepth--;
exprlist[block[currblockdepth]].end = nexpr - 1;
}
;
loop_control : FOR '(' expression ';' relation ';' expression ')'
{
exprlist[nexpr].expr = make_for($3, $5, $7);
exprlist[nexpr].start = nexpr + 1;
exprlist[nexpr].indent = currblockdepth;
block[currblockdepth] = nexpr;
currblockdepth++;
nexpr++;
}
;
relation : expression '<' expression
{
$$ = make_node(OPLE, $1, $3);
}
for 文の出力のときには、このブロック内の式を一度に出力するようにします。
static void
write_block(int *curr)
{
int i, start, end;
start = exprlist[*curr].start;
end = exprlist[*curr].end;
write_scalar_node(exprlist[*curr].expr);
printf("\n");
i = start;
while (i <= end) {
if (exprlist[i].expr->opcode == OPFOR) {
write_block(&i);
} else if (exprlist[i].expr->type == FLOAT) {
set_indent(exprlist[i].indent + 1);
write_scalar_node(exprlist[i].expr);
printf(";\n");
} else {
set_indent(exprlist[i].indent + 1);
write_node(exprlist[i].expr);
}
i++;
}
set_indent(exprlist[*curr].indent);
write_indent();
printf("\t}\n");
*curr = end;
}
color 型の初期化子
ベクトル型の初期化文、
color white = color(1.0, 1.0, 1.0);
の実装がまだだったので、これを行います。 primary に triple を追加します。
primary ...
| triple
{
}
...
triple : '(' expression ',' expression ',' expression ')'
{
$$ = make_triple($2, $4, $6);
}
新しくデータ型 triplenode_t を作成し、make_triple() では左の子をこのデータにします。
...
#define TRIPLE 403
typedef struct _triplenode_t
{
node_t *nodes[3];
} triplenode_t;
make_triple(node_t *n1, node_t *n2, node_t *n3)
{
node_t *np;
triplenode_t *triple;
triple = (triplenode_t *)malloc(sizeof(triplenode_t));
triple->nodes[0] = n1;
triple->nodes[1] = n2;
triple->nodes[2] = n3;
np = new_node(TRIPLE, (node_t *)triple, NULL);
np->type = VECTOR;
np->tmpvar = new_tmpvar(np->type);
return np;
}
減算子と除算子の追加
これらは加算子や乗算子と同じように実装するだけです。
clouds.c
clouds.sl は最終的に以下のCのコードに変換されます。
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
DLLEXPORT void
clouds_initparam(ri_parameter_t *param)
{
double Kd;
double Ka;
Kd = 0.800000 ;
Ka = 0.200000 ;
ri_param_add(param, "Kd", TYPEFLOAT, &Kd);
ri_param_add(param, "Ka", TYPEFLOAT, &Ka);
}
DLLEXPORT void
clouds(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
double Kd;
double Ka;
double sum;
double i;
double freq;
ri_vector_t tmp0;
ri_color_t white;
double tmp1;
double tmp2;
double tmp3;
double tmp4;
double tmp5;
ri_vector_t tmp6;
ri_vector_t tmp7;
ri_vector_t tmp8;
double tmp9;
double tmp10;
double tmp11;
double tmp12;
double tmp13;
double tmp14;
ri_color_t tmp15;
ri_color_t tmp16;
ri_param_eval(&Kd, param, "Kd");
ri_param_eval(&Ka, param, "Ka");
ri_vector_set(tmp0, 1.000000, 1.000000, 1.000000, 1.0);
ri_vector_copy(white, tmp0);
sum = 0.000000 ;
freq = 4.000000 ;
for (i = 0.000000 ; i < 6.000000 ; i = i + 1.000000 ) {
ri_vector_set(tmp6, freq, freq, freq, 1.0);
ri_vector_mul(tmp7, tmp6, status->input.P);
sum = sum + 1.000000 / freq * fabs( 0.500000 - noise3d(tmp7) ) ;
freq = 2.000000 * freq ;
}
mixv(tmp15, status->input.Cs, white, sum * 4.000000 );
ri_vector_copy(output->Ci, tmp15);
ri_vector_set(tmp16, 1.000000, 1.000000, 1.000000, 1.0);
ri_vector_copy(output->Oi, tmp16);
}
なんか使われない一時変数の宣言がありますが、最適化は後に回すということで。
組み込み関数の追加
abs() については、C言語の fabs() を使用します。
mix() については、float型、ベクトル型がありますが、今回はベクトル型のみを実装し、 mixv() とします。
noise() については、以前日記で述べたように、 パーリンノイズを利用します。ソースコードは Ken Perlin 氏のホームページから取得します。今回はベクトル型を取ってスカラ値を返すノイズ関数、 noise3d()とする、 のみを実装します。 noise3d() は noise3() を呼ぶようにします。オリジナルの noise3() では [-1, 1] の範囲で値を返すので、これを [0,1] の範囲で返すように変更を加えておきます。
ソースコード
コンパイル、実行
シェーダコンパイラをビルドし、Cのシェーダソースを出力します。
$ flex tut15.l $ bison -y -d -t tut15.y $ gcc y.lex.c yy.tab.c sym.c tree.c -lfl $ ./a.out clouds.sl > clouds.c
シェーダコードが DSO に変わったので、シェーダコードのコンパイルとレンダラとの組み合わせは、OS ごとに異なります。
前回を参照してください。
上記の画像のようにもやのかかった画像がレンダリングされることを確認してください。

今回は if 文を実装します。
if 文の例として、前回と同じく「実践CGへの誘い」から checker.sl シェーダを選ました。
surface
checker(float Kd = .5,
Ka = .1,
frequency = 10;
color blackcolor = color(0, 0, 0))
{
float smod = mod(s * frequency, 1),
tmod = mod(t * frequency, 1);
if (smod < 0.5) {
if (tmod < 0.5) {
Ci = Cs;
} else {
Ci = blackcolor;
}
} else {
if (tmod < 0.5) {
Ci = blackcolor;
} else {
Ci = Cs;
}
}
Oi = Os;
Ci = Oi * Ci * (
Ka * ambient() +
Kd * diffuse(faceforward(normalize(N), I)));
}
if 文
RenderMan シェーダ言語の if 文は、else は最大でも一つ、つまり else if 節は存在しません。
else のブロックにも対応できるように、ブロックを記憶するデータ構造を以下のように変更します。
typedef struct _exprblock_t
{
node_t *expr;
int start; /* block start */
int end; /* block end */
int elsestart; /* else block start */
int elseend; /* else block end */
int indent;
} exprblock_t;
if 文の構文ルールは以下のようになります。
statement ...
| if_state
{
int pos;
currblockdepth--;
pos = block[currblockdepth];
exprlist[pos].elsestart = 0;
exprlist[pos].elseend = 0;
}
| if_state ELSE statement
{
int pos;
currblockdepth--;
pos = block[currblockdepth];
exprlist[pos].elseend = nexpr - 1;
}
...
if_state : if_control statement
{
int pos;
pos = block[currblockdepth - 1];
exprlist[pos].end = nexpr - 1;
exprlist[pos].elsestart = nexpr;
exprlist[pos].elseend = 0;
}
;
if_control : IF '(' relation ')'
{
exprlist[nexpr].expr = make_if($3);
exprlist[nexpr].start = nexpr + 1;
exprlist[nexpr].indent = currblockdepth;
block[currblockdepth] = nexpr;
currblockdepth++;
nexpr++;
}
;
if ... else ... 型の構文ルールは、bison の性質上、シフト還元衝突のワーニングがどうしても出てしまいます。しかし括弧を省略せずに記述しておけば、間違った構文解析になることはないでしょう。この現象の詳細については、bison のマニュアルの 5.2 を参照してください。
write_block() では、else 文のブロックも出力するように変更します。
static void
write_block(int *curr)
{
int i, indent;
int start, end;
int elsestart, elseend;
start = exprlist[*curr].start;
end = exprlist[*curr].end;
elsestart = exprlist[*curr].elsestart;
elseend = exprlist[*curr].elseend;
indent = exprlist[*curr].indent;
set_indent(indent);
write_scalar_node(exprlist[*curr].expr);
printf("\n");
i = start;
while (i <= end) {
if (exprlist[i].expr->opcode == OPFOR ||
exprlist[i].expr->opcode == OPIF) {
write_block(&i);
} else if (exprlist[i].expr->type == FLOAT) {
set_indent(indent + 1);
write_scalar_node(exprlist[i].expr);
printf(";\n");
} else {
set_indent(indent + 1);
write_node(exprlist[i].expr);
}
i++;
}
set_indent(indent);
write_indent();
printf("\t}");
if (elseend != 0) {
printf(" else {\n");
i = elsestart;
while (i <= elseend) {
if (exprlist[i].expr->opcode == OPFOR ||
exprlist[i].expr->opcode == OPIF) {
write_block(&i);
} else if (exprlist[i].expr->type == FLOAT) {
set_indent(indent + 1);
write_scalar_node(exprlist[i].expr);
printf(";\n");
} else {
set_indent(indent + 1);
write_node(exprlist[i].expr);
}
i++;
}
set_indent(indent);
write_indent();
printf("\t}");
}
printf("\n");
set_indent(indent);
if (elseend == 0) {
*curr = end;
} else {
*curr = elseend;
}
}
ソースコード
コンパイル、実行
シェーダコンパイラをビルドし、シェーダのCコードを出力します。
$ flex tut16.l $ bison -y -d -t tut16.y <-- ここでワーニングが1つ出力されますが、これは無視してください $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out checher.sl > checher.c
checker.c は以下のようになります。
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
DLLEXPORT void
checker_initparam(ri_parameter_t *param)
{
double Kd;
double Ka;
double frequency;
ri_vector_t tmp0;
ri_color_t blackcolor;
Kd = 0.500000 ;
Ka = 0.100000 ;
frequency = 10.000000 ;
ri_vector_set(tmp0, 0.000000, 0.000000, 0.000000, 1.0);
ri_vector_copy(blackcolor, tmp0);
ri_param_add(param, "Kd", TYPEFLOAT, &Kd);
ri_param_add(param, "Ka", TYPEFLOAT, &Ka);
ri_param_add(param, "frequency", TYPEFLOAT, &frequency);
ri_param_add(param, "blackcolor", TYPEVECTOR, blackcolor);
}
DLLEXPORT void
checker(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
double Kd;
double Ka;
double frequency;
ri_color_t blackcolor;
double tmp1;
double tmp2;
double smod;
double tmp3;
double tmp4;
double tmod;
double tmp5;
double tmp6;
double tmp7;
double tmp8;
double tmp9;
double tmp10;
ri_color_t tmp11;
ri_vector_t tmp13;
ri_vector_t tmp12;
ri_vector_t tmp14;
ri_vector_t tmp18;
ri_vector_t tmp15;
ri_vector_t tmp16;
ri_vector_t tmp17;
ri_vector_t tmp19;
ri_vector_t tmp20;
ri_vector_t tmp21;
ri_color_t tmp22;
ri_param_eval(&Kd, param, "Kd");
ri_param_eval(&Ka, param, "Ka");
ri_param_eval(&frequency, param, "frequency");
ri_param_eval(blackcolor, param, "blackcolor");
smod = fmod( status->input.s * frequency , 1.000000 ) ;
tmod = fmod( status->input.t * frequency , 1.000000 ) ;
if (smod < 0.500000 ) {
if (tmod < 0.500000 ) {
ri_vector_copy(output->Ci, status->input.Cs);
} else {
ri_vector_copy(output->Ci, blackcolor);
}
} else {
if (tmod < 0.500000 ) {
ri_vector_copy(output->Ci, blackcolor);
} else {
ri_vector_copy(output->Ci, status->input.Cs);
}
}
ri_vector_copy(output->Oi, status->input.Os);
ri_vector_mul(tmp11, output->Oi, output->Ci);
ri_vector_set(tmp13, Ka, Ka, Ka, 1.0);
ambient(tmp12);
ri_vector_mul(tmp14, tmp13, tmp12);
ri_vector_set(tmp18, Kd, Kd, Kd, 1.0);
normalize(tmp15, status->input.N);
faceforward(tmp16, tmp15, status->input.I);
diffuse(tmp17, tmp16);
ri_vector_mul(tmp19, tmp18, tmp17);
ri_vector_add(tmp20, tmp14, tmp19);
ri_vector_copy(tmp21, tmp20);
ri_vector_mul(tmp22, tmp11, tmp21);
ri_vector_copy(output->Ci, tmp22);
}
レンダラとの組み合わせは、DSOの回を参照してください。
上記のチェッカー模様の画像が出力されるのを確認してください。
while 文の実装は、ほとんど for 文と変わりません。
clouds.sl を while 版に変更したものを例として、これを処理できるようにます。
surface
clouds(float Kd = .8,
Ka = .2)
{
float sum;
float i, freq;
color white = color(1.0, 1.0, 1.0);
sum = 0;
freq = 4.0;
i = 0;
while (i < 6) {
sum = sum + 1/freq * abs(.5 - noise(freq * P));
freq = 2 * freq;
i = i + 1;
}
Ci = mix(Cs, white, sum * 4.0);
Oi = 1.0;
}
while 文の構文ルールは for 文とほとんど同じです。
loop_control : FOR '(' expression ';' relation ';' expression ')'
{
exprlist[nexpr].expr = make_for($3, $5, $7);
exprlist[nexpr].start = nexpr + 1;
exprlist[nexpr].indent = currblockdepth;
block[currblockdepth] = nexpr;
currblockdepth++;
nexpr++;
}
| WHILE '(' relation ')'
{
exprlist[nexpr].expr = make_while($3);
exprlist[nexpr].start = nexpr + 1;
exprlist[nexpr].indent = currblockdepth;
block[currblockdepth] = nexpr;
currblockdepth++;
nexpr++;
}
;
あとの残りもほとんど for 文のときと同じように書くだけです。
ソースコード
コンパイル、実行
for 文の回と同じなのでそちらを参照してください。

前回で制御文の実装はだいたい終わりました。
今回は同じく「実践CGの誘い」から wood.sl を選び、これを処理するために必要な機能を実装し、機能拡張を行います。
surface
wood(float ringscale = 10;
color lightwood = color(0.3, 0.12, 0.03),
darkwood = color(0.05, 0.01, 0.005);
float Ka = 0.2,
Kd = 0.4,
Ks = 0.6,
roughness = 0.1)
{
point NN, V;
point PP;
float y, z, r;
NN = faceforward(normalize(N), I);
V = -normalize(I);
PP = transform("shader", P);
PP += noise(PP);
y = ycomp(PP);
z = zcomp(PP);
r = sqrt(y * y + z * z);
r *= ringscale;
r += abs(noise(r));
r -= floor(r);
r = smoothstep(0, 0.8, r) - smoothstep(0.83, 1.0, r);
Ci = mix(lightwood, darkwood, r);
Oi = Os;
Ci = Oi * Ci * (Ka * ambient() + Kd * diffuse(NN))
+ (0.3 * r + 0.7) * Ks * specular(NN, V, roughness);
}
wood.sl を処理するために新しく追加するのは、
o 加算代入("+="), 減算代入("-="), 乗算代入("*=")の演算子
o 関数 transform(), ycomp(), zcomp(), sqrt(), floor(), smoothstep()
o ベクトル式内のスカラ式
演算子の追加
加算代入("+="), 減算代入("-="), 乗算代入("*=")は、代入の実装と同じです。
assignexpression : IDENTIFIER '=' expression
{
var_reg($1, vartype);
$$ = make_node(OPASSIGN,
make_leaf($1),
$3);
}
| IDENTIFIER PLUSEQ expression
{
var_reg($1, vartype);
$$ = make_node(OPASSIGNADD,
make_leaf($1),
$3);
}
| IDENTIFIER MINUSEQ expression
{
var_reg($1, vartype);
$$ = make_node(OPASSIGNSUB,
make_leaf($1),
$3);
}
| IDENTIFIER MULEQ expression
{
var_reg($1, vartype);
$$ = make_node(OPASSIGNMUL,
make_leaf($1),
$3);
}
;
スカラ式のときはこれらの代入文はそのまま出力するようにします。
ベクトル式のときは、例えば以下のとき
P += A;
は、以下のようなコードを出力するようにします。
ri_vector_add(P, P, A);
関数の追加
transform(), ycomp(), zcomp(), sqrt(), floor(), smoothstep() を追加します。
sqrt(), floor() についてはC言語の関数を用いるようにします。
transform() は、とりあえず今は座標変換はしないで単純にコピーするだけにします。
void
transform(ri_vector_t dst, char *tospace, ri_vector_t src)
{
ri_vector_copy(dst, src);
}
ycomp(), zcomp() はベクトルの要素を返す関数です。
double
xcomp(ri_vector_t v)
{
return v[0];
}
double
ycomp(ri_vector_t v)
{
return v[1];
}
double
zcomp(ri_vector_t v)
{
return v[2];
}
smoothstep() 関数は、値が min 以下では 0 を、 値が max 以上では 1 を、そうでなければエルミート補間した値を返します。
double
smoothstep(double min, double max, double value)
{
double t;
if (value < min) {
t = 0.0;
} else if (value >= max) {
t = 1.0;
} else {
/* Hermite interpolation */
t = (value - min) / (max - min);
t = t * t * (3.0 - 2.0 * t);
}
return t;
}
ベクトル式内のスカラ式
たとえば、
PP += noise(PP);
は、右辺値がベクトル式、引数もベクトル式ですが noise() 関数のみスカラ値を返すスカラ式です。
また、
Ci = Oi * Ci * (Ka * ambient() + Kd * diffuse(NN))
+ (0.3 * r + 0.7) * Ks * specular(NN, V, roughness);
も全体ではベクトル式ですが、式の途中にスカラ式
(0.3 * r + 0.7) * Ks
があります。
今までのスカラ式の対応は単一の識別子のとき(たとえば Ka * ambient())のみだったので、上記のような複合式や関数でも対応できるようにします。
実装としては、
ri_vector_set(tmp, noise(PP), noise(PP), noise(PP), 1.0);
としてもよいのですが、式が長くなるので、
tmp[0] = noise(PP); ri_vector_set(tmp, tmp[0], tmp[0], tmp[0], 1.0);
とするようにします。
Cコード
最終的に、wood.sl は、以下のようなCのコードになります。
#include <stdio.h>
#include <stdlib.h>
#include "shader.h"
DLLEXPORT void
wood_initparam(ri_parameter_t *param)
{
double ringscale;
ri_vector_t tmp0;
ri_color_t lightwood;
ri_vector_t tmp1;
ri_color_t darkwood;
double Ka;
double Kd;
double Ks;
double roughness;
ringscale = 10.000000 ;
ri_vector_set(tmp0, 0.300000, 0.120000, 0.030000, 1.0);
ri_vector_copy(lightwood, tmp0);
ri_vector_set(tmp1, 0.050000, 0.010000, 0.005000, 1.0);
ri_vector_copy(darkwood, tmp1);
Ka = 0.200000 ;
Kd = 0.400000 ;
Ks = 0.600000 ;
roughness = 0.100000 ;
ri_param_add(param, "ringscale", TYPEFLOAT, &ringscale);
ri_param_add(param, "lightwood", TYPEVECTOR, lightwood);
ri_param_add(param, "darkwood", TYPEVECTOR, darkwood);
ri_param_add(param, "Ka", TYPEFLOAT, &Ka);
ri_param_add(param, "Kd", TYPEFLOAT, &Kd);
ri_param_add(param, "Ks", TYPEFLOAT, &Ks);
ri_param_add(param, "roughness", TYPEFLOAT, &roughness);
}
DLLEXPORT void
wood(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
double ringscale;
ri_color_t lightwood;
ri_color_t darkwood;
double Ka;
double Kd;
double Ks;
double roughness;
ri_vector_t NN;
ri_vector_t V;
ri_vector_t PP;
double y;
double z;
double r;
ri_vector_t tmp2;
ri_vector_t tmp3;
ri_vector_t tmp4;
ri_vector_t tmp5;
char * tmp6;
ri_vector_t tmp7;
ri_vector_t tmp8;
ri_vector_t tmp9;
ri_vector_t tmp10;
ri_vector_t tmp11;
ri_vector_t tmp12;
double tmp13;
double tmp14;
double tmp15;
double tmp16;
double tmp17;
double tmp18;
double tmp19;
double tmp20;
double tmp21;
double tmp22;
double tmp23;
double tmp24;
double tmp25;
ri_color_t tmp26;
ri_color_t tmp27;
ri_vector_t tmp29;
ri_vector_t tmp28;
ri_vector_t tmp30;
ri_vector_t tmp32;
ri_vector_t tmp31;
ri_vector_t tmp33;
ri_vector_t tmp34;
ri_vector_t tmp35;
ri_color_t tmp36;
double tmp37;
double tmp38;
double tmp39;
double tmp40;
ri_vector_t tmp42;
ri_vector_t tmp41;
ri_vector_t tmp43;
ri_color_t tmp44;
ri_param_eval(&ringscale, param, "ringscale");
ri_param_eval(lightwood, param, "lightwood");
ri_param_eval(darkwood, param, "darkwood");
ri_param_eval(&Ka, param, "Ka");
ri_param_eval(&Kd, param, "Kd");
ri_param_eval(&Ks, param, "Ks");
ri_param_eval(&roughness, param, "roughness");
normalize(tmp2, status->input.N);
faceforward(tmp3, tmp2, status->input.I);
ri_vector_copy(NN, tmp3);
normalize(tmp4, status->input.I);
ri_vector_copy(tmp5, tmp4);
ri_vector_neg(tmp5);
ri_vector_copy(V, tmp5);
tmp6 = "shader";
transform(tmp7, tmp6, status->input.P);
ri_vector_copy(PP, tmp7);
tmp9[0] = noise3d(PP) ;
ri_vector_set(tmp9, tmp9[0], tmp9[0], tmp9[0], 1.0);
ri_vector_add(PP, PP, tmp9);
y = ycomp( PP ) ;
z = zcomp( PP ) ;
r = sqrt( y * y + z * z ) ;
r *= ringscale ;
r += fabs( noise1d( r ) ) ;
r -= floor( r ) ;
r = smoothstep( 0.000000 , 0.800000 , r )
- smoothstep( 0.830000 , 1.000000 , r ) ;
mixv(tmp26, lightwood, darkwood, r );
ri_vector_copy(output->Ci, tmp26);
ri_vector_copy(output->Oi, status->input.Os);
ri_vector_mul(tmp27, output->Oi, output->Ci);
ri_vector_set(tmp29, Ka, Ka, Ka, 1.0);
ambient(tmp28);
ri_vector_mul(tmp30, tmp29, tmp28);
ri_vector_set(tmp32, Kd, Kd, Kd, 1.0);
diffuse(tmp31, NN);
ri_vector_mul(tmp33, tmp32, tmp31);
ri_vector_add(tmp34, tmp30, tmp33);
ri_vector_copy(tmp35, tmp34);
ri_vector_mul(tmp36, tmp27, tmp35);
tmp42[0] = ( 0.300000 * r + 0.700000 ) * Ks ;
ri_vector_set(tmp42, tmp42[0], tmp42[0], tmp42[0], 1.0);
specular(tmp41, NN, V, roughness);
ri_vector_mul(tmp43, tmp42, tmp41);
ri_vector_add(tmp44, tmp36, tmp43);
ri_vector_copy(output->Ci, tmp44);
}
ソースコード
コンパイル、実行
シェーダコンパイラをいつものようにビルドして、wood.c を作成します。
$ flex tut18.l $ bison -y -d -t tut18.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out wood.sl > wood.c
レンダラとの組み合わせは、DSOの回を参照してください。
上記の画像が出力されるのを確認してください。

ある程度までシェーダコンパイラの機能が出来てきましたので、ここらへんで PRMan 11 のグローバルイルミネーション拡張を実装していきたいと思います。
最初は、アンビエントオクルージョン(Ambient Occlusion)の実装です。 RenderMan シェーダ言語でアンビエントオクルージョンを行うには、gather() 関数によるループと、occlusion() 関数の2通りの方法があります。
SIGRAPH 2003 の RenderMan のコースノートによると、PRMan 11 の実装では、gather() はアンビエントオクルージョンに限らず一般的な半球上の光積分を行うのに対し、occlusion() はアンビエントオクルージョンの計算に最適化されているので、gather() ループよりも 10 倍ほど高速に計算できるそうです(放射照度キャッシュとか使ってるのかな)。
occlusion()
occlusion() 関数の定義は以下の通りです。
float occlusion(point P, normal N, float samples, ...)
occlusion() では、点 P から法線 N の方向の半球上にランダムに samples 個のレイ
を飛ばします。各レイがなんらかの物体に当たった場合は 1 、何にも当たらない場合は 0 としてその総計を n とした場合、返り値として n / samples を返します。つまり occlusion() の返り値はどれくらい遮蔽されているかを表します。値は[0,1]の範囲になります。
SIGGRAPH 2003 のコースノートから、occlusion() 関数を用いたシェーダの例を示します。
surface
occsurf2(float samples = 32)
{
float occ = occlusion(P, N, samples);
Ci = (1 - occ) * Cs * Os;
Oi = Os;
}
occlusion() 関数はオプションの引数を取れるように定義されていますが、今回は省略して実装することにします。
実装
occlusion() 内ではシーンのレイトレースがシェーダの実装内で行えるようにするために、シーンのレイトレース部分を render.c から独立させて新しく scene.c に作成します。
extern int scene_trace(double isect_pos[3],
double isect_normal[3],
double isect_uv[2],
double isect_col[3],
double org[3], double dir[3]);
今回は球に加えて床もシーンに加えておきました。
乱数
半球上にランダムにレイを飛ばすために乱数が必要になります。乱数にはC言語のライブラリではなく Merssenne Twister を用いることにします。
http://www.math.keio.ac.jp/~matumoto/mt.html
occlusion()の実装
occlusion() の実装は以下になります。
double
occlusion(ri_vector_t P, ri_vector_t N, double nsamples)
{
const double m_pi = 3.1415926535;
static int first = 1;
int i, j, k;
int coverage;
int ntheta, nphi;
double theta, phi;
ri_vector_t dir;
ri_vector_t raydir;
ri_vector_t basis[3];
ri_vector_t isectpos;
ri_vector_t isectnormal;
ri_vector_t isectuv;
ri_vector_t isectcol;
if (first) {
init_genrand(4332);
first = 0;
}
coverage = 0;
/* nphi = 3 * ntheta,
* nsamples = nphi * ntheta
*/
ntheta = nsamples / 3.0;
ntheta = (int)sqrt((double)ntheta);
if (ntheta < 1) ntheta = 1;
nphi = 3 * ntheta;
/* generate orhonormal basis with its z-axis corresponds to
* surdace normal.
*/
gen_ortho_basis(basis, N);
/* generate samples on hemisphere with
* Probability DistributionFunction = cos(theta)/Pi
*/
for (j = 0; j < nphi; j++) {
for (i = 0; i < (int)ntheta; i++) {
theta = sqrt(((double)i + genrand_real2())) /
(double)ntheta) ;
phi = 2.0 * m_pi * ((double)j + genrand_real2()) /
(double)nphi;
dir[0] = cos(phi) * theta;
dir[1] = sin(phi) * theta;
dir[2] = sqrt(1.0 - theta * theta);
for (k = 0; k < 3; k++) {
raydir[k] = dir[0] * basis[0][k]
+ dir[1] * basis[1][k]
+ dir[2] * basis[2][k];
}
ri_vector_normalize(raydir);
if (scene_trace(isectpos, isectnormal,
isectuv, isectcol,
P, raydir)) {
/* there is a occluder. */
coverage++;
}
}
}
if (coverage > (int)nsamples) coverage = (int)nsamples;
return (double)coverage / (double)nsamples;
}
nsamples は、緯度と経度に分解します。経度方向のサンプル数 nphi は緯度方向のサンプル数 ntheta の 3 倍になるようにします。
ランダムなレイの方向は、コサイン減衰を考慮したインポータンスサンプリング(importance sampling)で求めます(確率分布関数 = cos(theta)/ PI)。つまりは半球の頂点に向かってはより多くのサンプルを生成し、地平線方向では少ないサンプルになります。また層別サンプリング(stratified sampling)で[0,1)区間の乱数を生成します。
サンプルの方向は z 軸が上のローカル座標で生成されるので、これを法線方向 N に合わせるように座標変換を行います。このとき法線方向 N が z 方向となる基底ベクトル(つまりは回転行列)が必要になるので、 gen_ortho_basis() で計算します。
static void
gen_ortho_basis(ri_vector_t basis[3], const ri_vector_t n)
{
int i;
ri_vector_copy(basis[2], n);
basis[1][0] = basis[1][1] = basis[1][2] = 0.0;
for (i = 0; i < 3; i++) {
if (basis[2][i] < 0.6 && basis[2][i] > -0.6) break;
}
if (i >= 3) i = 0;
basis[1][i] = 1.0;
ri_vector_cross(basis[0], basis[1], basis[2]);
ri_vector_normalize(basis[0]);
ri_vector_cross(basis[1], basis[2], basis[0]);
}
ソースコード
コンパイル、実行
シェーダコンパイラをいつものようにビルドして、occsurf2.c を作成します。
$ flex tut19.l $ bison -y -d -t tut19.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out occsurf2.sl > occsurf2.c
レンダラとの組み合わせは、DSOの回を参照してください。os-x の場合はレンダラのコンパイルのときに scene.c mt19937ar-cok.c も追加するようにします。linux と windows の場合は shader の共有ライブラリ作成のときに scene.c mt199937ac-cok.c も追加するようにします。
上記の画像が出力されるのを確認してください。

occlusion() に続いて、レイトレースを行う関数 trace() を実装して反射効果を実現します。例題のシェーダ mirror.sl は以下のようになります。
surface
mirror(float Kd = 0.5)
{
color ref;
normal Nf = faceforward(normalize(N), I);
vector R = reflect(I, Nf);
ref = trace(P, R);
Ci = Kd * diffuse(Nf) * Cs + (1 - Kd) * ref;
Oi = Os;
}
trace() の定義は以下になります。
color trace(point P, point R)
trace() は、点 P から方向 R へとレイトレーシングを行います。レイトレーシングをサポートしない RenderMan 実装ではゼロ(黒色)が返されます。
また、最近の RenderMan 仕様 3.3(ドラフト版) では、
float trace(point P, point R)
も定義されています。こちらでは、色ではなくレイが物体にヒットした位置までのレイの長さが返されます。
今回の trace() 関数の実装にあたっての変更点は以下の通りです。
o 透視投影カメラの実装
o trace() の実装および再帰的なシェーダの呼び出し
透視投影カメラの実装
今まではずっと簡潔のために平行投影カメラでレンダリングを行ってきましたが、反射効果などは平行投影カメラでは効果が分かりにくいので、ここで透視投影カメラを実装することにします。
今回は OpenGL の gluLookAt() のように、視点の位置と視点のターゲット(視点が注目する位置)を指定することで、カメラ行列を作成します。
このカメラ行列を作成する関数 lookat() は、Mesa のソースコードから拝借しました。OpenGL は左手座標系なので、右手座標系にするなど少し変更を加えています。
static void
lookat(double m[16], double eyex, double eyey, double eyez,
double centerx, double centery, double centerz,
double upx, double upy, double upz)
{
double x[3], y[3], z[3];
double mag;
int i;
/* Make rotation matrix */
/* Z vector */
#if 0
z[0] = eyex - centerx;
z[1] = eyey - centery;
z[2] = eyez - centerz;
#else
/* right-handed version */
z[0] = centerx - eyex;
z[1] = centery - eyey;
z[2] = centerz - eyez;
#endif
mag = sqrt(z[0] * z[0] + z[1] * z[1] + z[2] * z[2]);
if (mag) { /* mpichler, 19950515 */
z[0] /= mag;
z[1] /= mag;
z[2] /= mag;
}
/* Y vector */
y[0] = upx;
y[1] = upy;
y[2] = upz;
/* X vector = Y cross Z */
x[0] = y[1] * z[2] - y[2] * z[1];
x[1] = -y[0] * z[2] + y[2] * z[0];
x[2] = y[0] * z[1] - y[1] * z[0];
/* Recompute Y = Z cross X */
y[0] = z[1] * x[2] - z[2] * x[1];
y[1] = -z[0] * x[2] + z[2] * x[0];
y[2] = z[0] * x[1] - z[1] * x[0];
/* mpichler, 19950515 */
/* cross product gives area of parallelogram, which is < 1.0 for
* non-perpendicular unit-length vectors; so normalize x, y here
*/
mag = sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
if (mag) {
x[0] /= mag;
x[1] /= mag;
x[2] /= mag;
}
mag = sqrt(y[0] * y[0] + y[1] * y[1] + y[2] * y[2]);
if (mag) {
y[0] /= mag;
y[1] /= mag;
y[2] /= mag;
}
#define M(row,col) m[col*4+row]
M(0, 0) = x[0];
M(0, 1) = x[1];
M(0, 2) = x[2];
M(0, 3) = 0.0;
M(1, 0) = y[0];
M(1, 1) = y[1];
M(1, 2) = y[2];
M(1, 3) = 0.0;
M(2, 0) = z[0];
M(2, 1) = z[1];
M(2, 2) = z[2];
M(2, 3) = 0.0;
M(3, 0) = 0.0;
M(3, 1) = 0.0;
M(3, 2) = 0.0;
M(3, 3) = 1.0;
#undef M
}
この行列をローカル座標で生成した視点レイに掛けることで、ワールド座標でのカメラの方向から見たレイへと変換することができます。
double fov = 45.0;
flen = 1.0 / tan((fov * 3.141592 / 180.0) * 0.5);
...
dir[0] = sx;
dir[1] = sy;
dir[2] = flen;
ctow(dir, m, dir); /* camera to world */
len = dir[0] * dir[0] +
dir[1] * dir[1] +
dir[2] * dir[2];
len = sqrt(len);
if (len != 0.0) {
dir[0] /= len;
dir[1] /= len;
dir[2] /= len;
}
fov は視野 (field of view) です。
trace() の実装および再帰的なシェーダの呼び出し
trace() は、レイの方向にヒットしたサーフェスの色を返すため、レイがヒットしたサーフェスでもシェーダを呼び出してサーフェスの色を計算する必要があります。もしこのヒットしたサーフェスに割り当てられているシェーダが trace() を含んでいると、さらにそこからまたレイトレーシングを行い、ヒットしたサーフェスでシェーダを呼び出さなければなりません。図示すると以下のようになります。

そのため、trace() からシェーダが呼び出せるように実装し、再帰的な構造をとれるようにする必要があります。
レイのデプスの追加
まず最初に、シェーダの実行コンテキストである ri_status_t に、現在のレイのデプス(深さ、反射回数)情報を持たせるようにして、一定のデプス以上であればレイトレーシングを打ち切るようにします。
typedef struct _ri_status_t
{
ri_input_t input;
int ray_depth;
} ri_status_t;
shader.c 内からシェーダをコールできるように、render.c で取得したシェーダへの関数ポインタのコピーをセットする関数を作成して、これを shader.c 内で保持するようにします。
/* shader.c */
static shader_initparamproc currshader_initparam = NULL;
static shaderproc currshader = NULL;
...
void
shader_set(shader_initparamproc initparam, shaderproc shader)
{
currshader_initparam = initparam;
currshader = shader;
}
これで trace() を実装する準備ができました。
trace() の実装
trace() の実装は以下になります。
void
trace(ri_vector_t dst, ri_vector_t P, ri_vector_t R)
{
ri_vector_t isectpos;
ri_vector_t isectnormal;
ri_vector_t isectuv;
ri_vector_t isectcol;
ri_vector_t eye;
ri_status_t newstatus;
ri_status_t *oldstatus;
ri_parameter_t param;
ri_output_t out;
if (currstatus->ray_depth > 3) {
dst[0] = 0.0;
dst[1] = 0.0;
dst[2] = 0.0;
return;
}
if (scene_trace(isectpos, isectnormal,
isectuv, isectcol,
P, R)) {
/* hit surface. */
ri_vector_sub(eye, isectpos, P);
ri_vector_normalize(eye);
status_copy(&newstatus, currstatus);
oldstatus = currstatus; /* push status */
newstatus.ray_depth++; /* increment ray depth */
ri_vector_copy(newstatus.input.Cs, isectcol);
ri_vector_copy(newstatus.input.P, isectpos);
ri_vector_copy(newstatus.input.I, eye);
ri_vector_copy(newstatus.input.E, P);
ri_vector_copy(newstatus.input.N, isectnormal);
newstatus.input.s = isectuv[0];
newstatus.input.t = isectuv[1];
currstatus = &newstatus;
memset(¶m, 0, sizeof(ri_parameter_t));
/* call shader */
currshader_initparam(¶m);
currshader(&out, currstatus, ¶m);
dst[0] = out.Ci[0];
dst[1] = out.Ci[1];
dst[2] = out.Ci[2];
currstatus = oldstatus; /* pop status */
} else {
dst[0] = 0.0;
dst[1] = 0.0;
dst[2] = 0.0;
}
}
まず、最初にレイのデプスをチェックして、現在のレイが 3 回以上跳ね返りを起こしている場合は処理を打ち切ります。
レイが物体にヒットしなかった場合はゼロ(黒色)を返します。
もしレイが物体にヒットした場合は、新しいコンテキスト newstatus を currstatus からコピーして作成します。
そしてレイのデプスをインクリメントし、またヒットしたサーフェスの情報でコンテキストを更新します。
現在のコンテキスト currstatus を待避させ、新しいコンテキスト newstatus を currstatus としてセットし、シェーダをコールします。シェーダのコールが終了したら、待避させていたコンテキストを元に戻します。
ソースコード
コンパイル、実行
シェーダコンパイラを作成して、mirror.c を作成します。
$ flex tut20.l $ bison -y -d -t tut20.y $ gcc lex.yy.c y.tab.c sym.c tree.c -lfl $ ./a.out mirror.sl > mirror.c
レンダラとの組み合わせは、DSOの回を参照してください。os-x の場合はレンダラのコンパイルのときに scene.c mt19937ar-cok.c も追加するようにします。linux と windows の場合は shader の共有ライブラリ作成のときに scene.c mt199937ac-cok.c も追加するようにします。
上記の画像が出力されるのを確認してください。
(なんか床に移り込んだ球の反射がおかしいような...)
wood.sl シェーダを、シェーダパラメータ(シェーダインスタンス変数)を変えてレンダリングしたもの。

まだグローバルイルミネーション機能の統合にはいたらずにローカルイルミネーション部分どまりです。
lucille は C シェーダとしてシェーダ言語を実装することにしていますが、 RenderMan シェーダもこの枠組みで内包するので、シェーダパラメータの扱いをどのようにするかという問題があります。シェーダパラメータはシェーダコード、RIB 両方で設定することが出来ますが、レンダラ側(RIB 側)からはシェーダの持つパラメータ変数の名前と型は見えないので、何らかの方法でパラメータ情報をやりとりする必要があります。
lucille では、このシェーダパラメータの扱いを、以下のようにして実装しています。
まず、RenderMan SL では、シェーダパラメータには必ずデフォルト値を指定する必要があります。これにより初期化コードを出力することができます。たとえば wood.sl の場合だと、
DLLEXPORT void
wood_initparam(ri_parameter_t *param)
{
...
ri_param_add(param, "ringscale", TYPEFLOAT, &ringscale);
ri_param_add(param, "lightwood", TYPEVECTOR, &lightwood);
ri_param_add(param, "darkwood", TYPEVECTOR, &darkwood);
ri_param_add(param, "Ka", TYPEFLOAT, &Ka);
ri_param_add(param, "Kd", TYPEFLOAT, &Kd);
ri_param_add(param, "Ks", TYPEFLOAT, &Ks);
ri_param_add(param, "roughness", TYPEFLOAT, &roughness);
}
のようなコードを用意して、 param にパラメータ変数を登録します。param は単純なハッシュ構造です。これをレンダラが Surface プロトコルを呼ぶ時にまず最初に呼び出し、その後 RIB 側での Surface プロトコルが呼ばれた時のインスタンス変数の設定で上書きします。
RiSurfaceV(RtToken name, RtInt n, RtToken tokens[], RtPointer params[])
{
....
param = ri_param_new();
initparamproc(param);
for (i = 0; i < n; i++) {
ri_param_override(param, tokens[i], params[i]);
}
}
初期化コードでの変数の追加時に変数の型情報も保持しておくことで、 ri_param_override() のコールで型を指定する必要はありません(RIB 側では Declare を使わない限りパラメータの型を指定することも知ることもできない)。あとはこの param をシェーダコードに渡してあげれば、デフォルト値を RIB での設定で上書きされたパラメータ値のリストを利用する事ができます。
たとえば、 wood.sl の Ka のデフォルト値は 0.2 です。初期化コードでは Ka は 0.2 に設定されます。その後 RIB 側で、
Surface "wood" "Ka" [ 0.8 ]
とあると、 ri_param_override() で Ka が 0.8 に上書きされ、シェーダコード内、
DLLEXPORT void
wood(ri_output_t *output, ri_status_t *status, ri_parameter_t *param)
{
float Ka;
ri_param_eval(&Ka, param, "Ka");
...
では、 Ka を評価すると 0.8 が返されるようになります。
パラメータ参照の最適化
今のところでは、パラメータ変数の設定、評価には文字列によるハッシュ参照を用いて実装しています。
シェーダコードが呼ばれるたびに、パラメータ変数の値を評価するのに文字列からハッシュ参照するのは結構パフォーマンスを食うと思います。シェーダパラメータ変数は増えたり減ったりすることはないので、たとえば完全ハッシュ(Perfect hash)を用いることで、少しは文字列でのハッシュ参照を高速にすることができるかと思います。また C レベルでのコードへのユーザの介入を行わない前提であれば、文字列に一意の整数のインデックスを割り当てて、このインデックスから配列参照するようにすれば文字列でのハッシュ参照を行わなくてよくなります。
トランスレータではインデックスベースのコードを生成し、ユーザが直接 C シェーダコードを記述する場合は文字列ベースでも可能、という 2 種類のアクセス方法を提供するのもよいかもしれません。
--
シェーダ言語を実装するにあたり、RenderMan インターフェイス仕様書と「実践 CG への誘い」をまた詳しく読み始めているのですが、座標空間の扱いや微分関数など実装しなければならないコトがどんどん見つかってきて、なかなかすべてを実装するのは大変そうです。まあでも PRMan でさえすべてをサポートしているわけではないですしね。
それにしても先人たち(PRMan 開発者)はこのシェーダ言語のプログラムを 10 年以上も前に作っていたんだよなぁ、すごいです。「実践 CG への誘い」の内容もまだまだ色あせていないし。
過去にやった RenderMan シェーダ言語実装 ネタを元に、 シェーダ言語機能を lucille へ統合しています。基本的な枠組みはマージできてきました。とりあえず「実践 CG への誘い」にある clouds.sl シェーダを用いてテストレンダリングしたもの。

lucille は RenderMan SL -> RSL to C トランスレータ -> C シェーダコード -> コンパイル -> ダイナミックリンクロード(プラグインロード) という形でシェーダ言語機能を搭載することにしています。この C シェーダの利点と欠点をいくつか挙げてみました。
利点
o C プログラムになるので C のライブラリとか使えて拡張しやすい。
o C コンパイラでコンパイルするので、最適化はコンパイラが行ってくれる。またネイティブコード実行になるので高速。
o シェーダを実行するためのスタックマシン実行環境や仮想マシン実行環境を実装する必要がない。
欠点
o C コンパイラが必要。
o OS によって再コンパイルが必要(異なる OS 間での並列計算時に大変)
o シェーダがセグメンテーション違反で落ちるとレンダラも落ちる(mentalray でも指摘されているように)。
シェーダが落ちるとレンダラも落ちてしまうというのは、レンダラにフォールトトレランスが求められるプロダクションワークでは結構重要かもしれません。
RenderMan シェーディング言語を実装中ですが、参考にしている仕様書 3.2 を見ていたら inversesqrt() 関数というのがありました。
これは 仕様書 3.1 では見つかりませんでした。近年の CPU では逆平方根命令がサポートされているので、その影響でしょうか。
それにしても良く読むと、 3.1 と 3.2 では結構シェーディング言語の機能や組み込み関数が拡張されています(0.1 しか違わないけど、10 年近く隔たっているから当然ではありますが)。
機能のほうでは、配列のサポート、行列型(matrix)など。
組み込み関数のほうでは、cellnoise() などのノイズ系など。
3.3(ドラフト) はさらに G.I. 系の拡張も含まれるし...
とりあえずは 3.1 ベースで実装です。
シェーダを DLL(DSO) であつかえるように、 lucille のコンパイル方法を変える必要がありました。ここらへんの各種 OS のダイナミックリンクの仕方を調べなおすことになって結構時間を食ってしまいました(このときリンカの仕組みの参考になる Linkers and Loaders を昔持っていたのだけれど、どこかに行ってしまって探せずじまい)。
とりあえず今はどの OS でもうまく動いているようです。以下は lucille でダイナミックリンクを扱うときのメモ。
まず、前提条件として、
o DLL(DSO) を呼ぶレンダラ(lsh) は単一の実行バイナリ(つまり lsh はシステムのライブラリを除いて複数の共有ライブラリから構成されてはいない)
o シェーダ DLL は lsh の子(一部)にあたる(つまり lsh へのプラグイン)
を崩さずに、この枠組みでうまくシェーダ DLL を取り扱えるようにするのが目的でした。
キモは実行バイナリ lsh にシェーダ関数を含め、これらのシンボルをいかにしてエクスポートするか、でした。シェーダには lsh に含まれるシェーダ関数への参照があるのですが、このシェーダ関数の実態はコンパイル時にはわからず、 DLL ロード時になって初めて lsh から提供されて "見え"ます。このシンボルの解決をどうしたらよいかが問題になります。
Mac OS X(Panther) の場合
ダイナミックリンカが結構優秀なよう(それともルーズ?)なので、特に変更はありません。
通常の方法で実行バイナリを作成しておき、シェーダのコンパイルは以下になります(以下の例はデバッグ版、matte シェーダ)。
gcc -DHAVE_CONFIG_H -g -bundle -flat_namespace \ -undefined suppress -o matte.so matte.c
ただし lsh にはシェーダ関数が必ず含まれるようにしておかなければなりません。シェーダ関数はシェーダのみで使われ、 lsh 自体では使われることがないものもあるので、最適化でコードが削除されてしまう可能性があるからです。
linux(elf) の場合
実行バイナリのコンパイル時に -rdynamic を指定することで、実行バイナリに含まれているシンボルがエクスポートできてダイナミックリンク時に"見え"るようになるようです。
$ gcc -rdynamic -o lsh lsh.o ...
シェーダのコンパイルは以下のようになります(以下の例はデバッグ版、matte シェーダ)。
$ gcc -DHAVE_CONFIG_H -g -fPIC -c matte.c $ ld -export-dynamic -shared -o matte.so matte.o
Windows(Visual Studio) の場合
シェーダが参照しているシェーダ関数への参照はすべて DLL 作成時に解決されている必要があります。これにはインポートライブラリ(関数名だけが書かれて関数の実態コードは無いもの)を作成する必要があります。
いくつかインポートライブラリの作成方法はありますが、結局のところ、これはソースコードでシェーダ関数に __declspec(dllexport) をつけることで解決しました。
これにより、インポートライブラリ lsh.lib が作成されます。これを元にしてシェーダのDLL(以下の例はデバッグ版、matte シェーダ)は、
> cl /c /MDd /nologo /DDEBUG /DWIN32 matte.c > link /nologo /DLL /defaultlib:LIBC.LIB /OUT:matte.dll matte.obj lsh.lib
で作成されます。
RenderMan シェーディング言語(RSL)におけるサーフェスシェーダには、illuminance() ブロック(ループ)と呼ばれる、光積分を行なう特殊なブロック構文があります。
illuminance(point position, vector axis, float angle)
statements
illuminance() ブロックでは、シェーディング点の半球上に入射してくる光(ライト)すべてに対して反復して、ブロック内の式を実行します。
たとえばライトが 2 個、 RIB で指定されていたら(LightSource)、 2 回反復してブロック内が実行されます。
(ここらへんのライトのサンプリングなどの挙動は、レンダラ側で行なわれ、シェーダは光積分の詳細を考慮する必要はありません)
RSL で用意されている diffuse() 関数は、 illuminance() ブロックで以下のように記述することも可能です。
Nn = normalize(N);
illuminance(P, Nn, PI/2) {
Nn = normalize(L);
Ci += Cs * Cl * Ln.Nn;
}
ここで、 illuminance() ブロックでは、新たに L と Cl という 2 つのシェーダ変数を利用することができます(illuminance ブロック内でのみ利用可能)。
L はライトの方向になり、Cl はライトの色になります。
illumiannce ブロックは、つまりは光積分を行なう構文であるので、
グローバルイルミネーションを行なう上でも実装すべき重要な構文です。
しかし、この illumiance() ループは、 C 言語には無い構文なので、lucille での実装にはちょっとしたトリックが必要になりました。
最終的に、lucille では以下のようにして illuminance ブロックの実装を解決する方向としました。
illuminance() 実装
まず、lucille では、 illuminance() ループを以下のように for 文へと変換する戦略を取ることにしました。
ri_lightsource_t *light;
for (light = next_lightsource(status, &status->input.P, &Nf, 3.141592/2.0);
light != NULL;
light = next_lightsource(status, &status->input.P, &Nf, 3.141592/2.0)) {
ndotl = ri_vector_dot3(&light->L, &Nf);
ri_vector_copy(&tmp1, &light->Cl);
...
}
ri_lightsource_t は、ライトの情報(L と Cl など)を保持する特別な構造体です。
next_lightsource() では、レンダラ側で管理しているライトのリストを 1 つづつ取り出していく関数です。
ここで、L と Cl はシェーディング時に一定なので、
もしシェーダが next_lightsource() を最初に呼び出したときは、
ライト(方向光源)のリストを生成し、
レンダラ側に保存しておくようにします。
ri_lightsource_t *
next_lightsource(const ri_status_t *status,
const ri_vector_t *P, const ri_vector_t *N, float angle)
{
int tid; /* thread number */
tid = status->thread_num;
if (!glight_initialized[tid]) {
init_lightsource(status, P, N, angle);
glight_initialized[tid] = 1;
}
...
}
status->thread_num は、シェーダをマルチスレッドで実行するので、
ステータスに割り当てたゼロから始まるスレッドの番号が格納されている変数です。
スレッド固有データへのアクセスを、パフォーマンスのために pthread などの API を用いずに、
配列で行なって実現するために用いられます。
ライトのリストの生成は、シェーダ実行時に RIB で登録されているライトの情報
から基に生成されるべきですが、今回は実装の簡便のために、
まずは、半球上に適当な数のライトのサンプルを生成するようにしました( init_lightsource() )。
つまりは環境マップによる IBL みたいな感じですね。
これを実行すれば、アンビエントオクルージョンと同等のシェーディング結果となります。
以上のようにして、 lucille では illuminance() ブロックの実装を解決しました。
shader.c の next_lightsource() および init_lightsource() を参照。
ライトのリストの生成には、最適化の余地が多くあります。
RenderMan シェーダコンパイラも、ぼちぼちできてきました。
というわけで、lucille を使って、
RenderMan シェーダ言語チュートリアルみたいなこと(1 日 1 シェーダとかね)でもやっていこうかと思います。
実行するために、RenderMan シェーダ言語に対応した lucille のベータバイナリ版と以下のチュートリアルで使うファイルを
に置いときました。シェーダを動かせるぐらいでコンパイルしてあるので、普通にレンダラとして使うことはできないです。
とりあえずベータバイナリは Mac OS X(Panther, G4 以上) に限定します。
基本的に他の RenderMan レンダラでもできることなので、
以下を他の RenderMan レンダラでやることもできるでしょう
テクスチャマッピング
RenderMan が他のレンダラと違ってちょっと特殊なのは、
テクスチャマッピングもシェーダに含まれるということです。
そのため、RIB シーンファイルのみではテクスチャマッピングを行なう事ができません。
(RenderMan にとっかかり始めのころは、この不自由さにとまどいます)
lucille では、RIB に特別なトークンを指定して RIB 内でもテクスチャマッピングを行なうことができるように
ちょっと拡張がされていますが、ここではその機能は使わずに、シェーダでちゃんと行ないましょう。
テクスチャマッピングを行なうシェーダです。
/*
* texturemapping.sl
*/
surface
texturemapping(string texturename = "")
{
Ci = Os * ( Cs * color texture(texturename));
Oi = Os;
}
ここでシェーダパラメータの texturename は RIB ファイル内の Surface プロトコルで指定します。
Surface "texturemapping" "texturename" ["mudah.hdr"]
このように記述されていると、シェーダが呼び出されたときに、
texturename には mudah.hdr テクスチャファイル名が代入されます。
シェーダ内の texture(texturename) では、テクスチャをフェッチする関数なので、 mudah.hdr テクスチャがサンプルされます。
このシェーダを用いてレンダリングを行なうと、以下の画像が出力されます。

コマンドの手続きの詳細は、ベータバイナリ配布物の README_jp.txt にあります。
PP はパブリックドメインの C プリプロセッサです。
aqsis や Pixie のシェーダコンパイラの部分のソースで使われているのを見て知りました。
lucille のシェーダコンパイラには、まだプリプロセッサ機能は実装されていません。
なので、 PP はパブリックドメインだし、コードも小さいし、C 言語のコードなのでこれを組み込んでプリプロセッサ機能を実現しようと思います。
シェーダ言語ではそれほど複雑なプリプロセッサ機能は定義されていませんので、 PP で十分事足ります。また自作のコンパイラなど、単純なプリプロセッサ機能しか必要としないコンパイラの作成にも使えるでしょう。