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 -