ランバート反射(最小限)

参考図書
CによるCGレイトレーシング (Information & computing) 単行本 – 1992/8/1村岡一信 (著), 千葉則茂 (著)


3次元の球に光を当てた時の光の反射について考えます。

しかし真面目に3次元データを表示することを考える場合、全部自分で実装しようとするとあれやこれやとめんどくさく、ありものを使うとそもそも反射部分を実装する必要すらなくなってしまいます。

なのでここでは2次元で強引に再現し、反射部分の学びに集中します。

まずはランバート反射の式

$$
I_d=k_dI_p(\bold L \cdot \bold N)
$$

$${I_d}$$は球表面の輝度(0-255)
この値が最終出力として表示すべきもの。出力255の時、最も強く光が反射している状態。その色は白。

$${k_d}$$は拡散反射係数(0-1)
$${I_p}$$は光源の輝度(0-255)
この2つは入力パラメータ。

$${\bold L}$$は球表面から光源に向かうベクトル。
このベクトルの長さが小ならば光は球の局部を照らす。長さが大ならば光は球を広めに照らす。

$${\bold N}$$は球表面の単位法線ベクトル。

$${\bold L \cdot \bold N}$$は両ベクトルの内積です。成分毎の積の総和となり、出力はスカラーです。

難しく考える場合

球が3次元モデルとして与えられている場合、モデルの頂点を走査するなり面を走査するなりして、ループ毎にランバート反射式を求めていきます。単純に考えるなら、求めた輝度で面を塗ります。
この辺は結局使用する3次元モデルのデータ構造にもよるのでなんともいえませんが、例えば頂点を走査する場合、$${\bold L}$$は光源の位置ベクトルから頂点の位置ベクトルを引くことによって求まります。頂点の位置ベクトルはそのまま用いずとも面の重心座標を使っても何でもよく、ある程度好きにして良いものです。

法線を求めるには面を定義するための2つのベクトルが必要となり、それらの外積によって求まります。3次元モデルデータが3つの頂点からなるポリゴンであるならば1つの頂点から他の2つの頂点に向かうベクトルが面を定義します。
外積が面に対してどちらに向くかはプログラマが勝手にというか適切に設定するものです。また、球に対して内に向くか外に向くかはモデルデータを適切に並べる必要があります。

最低限頂点3つがあれば$${\bold L}$$と$${\bold N}$$は求まるわけなので、ループの最中にその場でインデックスを計算して求めてもいいし、クラス化して参照をたぐっていっても良いです。何でも良いです。

簡単に考える場合

$${z=f(x,y)}$$なる式が用意できるのであれば、少なくとも単純な凸凹してない形状に関しては3次元モデルデータを用意する必要もありません。2次元ベクトルのリストを走査すれば、ループ毎に図形の表面座標を求めることができます。$${\bold L}$$を求めることができます。

つまり単純な球、というか半分の球ならば$${\bold L}$$を求めるのに2次元ベクトルのリストがあれば良いということです。

また球の法線は、球の中心座標から球の各点に向かうベクトルです。なのでこのケースでは面の情報すらいりません。

以上のことから、
①縦横にグリッドを切って2回のforループによってループ毎に$${z=f(x,y)}$$を求める。ただし球、というか円からはみ出てる(x,y)は無視。
②光源の位置と球表面の座標(x,y,z)から$${\bold L}$$を求める。
③球の中心点と光源の位置から$${\bold N}$$を求める。
④ランバート反射式によってその球表面の座標(x,y,z)における輝度を求める。

ことにより、最小限の実装でランバート反射を多分試すことができます。多分。また、色をつけたり、環境光を足してみたり、反射モデルを変えたり、形状モデルを変えてみたりもできます。多分。


import java.util.List;


List<PVector>Vertices = new ArrayList<PVector>();
float Vertices_r = 20;

PVector obj;
float obj_r = 200;

PVector light_xy;
PVector light_z_gui;//光源のz座標のGUI

PVector lightI_gui;//光源の輝度(0-255)のGUI;
PVector k_gui;//拡散係数(0-1)のGUI

float grid_sep = 100;//gridの分割数


void setup()
{
  size(500,500);
  
  obj = new PVector(250,250);
  light_xy = new PVector(250,250);
  
  light_z_gui = new PVector(0,250);
  lightI_gui = new PVector(0,250);
  k_gui = new PVector(0,250);
  
  Vertices.add(obj);
  Vertices.add(light_xy);
  Vertices.add(light_z_gui);
  Vertices.add(lightI_gui);
  Vertices.add(k_gui);
}



void draw()
{
   background(255);
   
   fill(255);   
   circle(obj.x,obj.y,obj_r*2);
   
   float light_z = ((height/255)*light_z_gui.y)%255;//GUIから光源のz値を取得
   PVector L = light_xy.copy().sub(obj);
   L.z = light_z;//光源3次ベクトル(x,y,z)作成

   float Ip = ((height/255)*lightI_gui.y)%255;//GUIから光源の輝度を取得
   float kd = (k_gui.y)/height;//GUIから拡散係数を取得
   
   //円を囲う矩形(grid領域)の左上端
   PVector obj_lt = new PVector(obj.x-obj_r,obj.y-obj_r);
   
   float grid_unit = obj_r*2/grid_sep;
   
   for(int i = 0; i<grid_sep; i++)
   {
     float gx = obj_lt.x+i*grid_unit;
     for(int j = 0; j<grid_sep; j++)
     {
       float gy = obj_lt.y+j*grid_unit;
       
       //球に入ってない場合は弾く
       if(!PointInCircle(obj,obj_r,gx,gy)){continue;}
              
       PVector sc = new PVector(obj.x,obj.y,0);//球の中心
       float gz = HalfSphere(sc,obj_r,gx,gy);//(x,y)に応じた球のz値
       PVector sphere_v = new PVector(gx,gy,gz);//球表面の座標
       PVector N = sphere_v.sub(sc).normalize();//球表面の単位法線                    
       float Id = kd*Ip*Dot3(L,N);  //球表面輝度
       
       noStroke();
       fill(Id);
       rect(gx,gy,grid_unit,grid_unit);
       
     }     
   }
   
   stroke(0);
   fill(255);
   circle(light_xy.x,light_xy.y,20);   
   circle(light_z_gui.x,light_z_gui.y,20);   
   circle(lightI_gui.x,lightI_gui.y,20);   
   circle(k_gui.x,k_gui.y,20);   
   fill(0);
   text("LIGHT_Z : "+ str(light_z),light_z_gui.x,light_z_gui.y);
   text("LIGHT_I : "+ str(Ip),lightI_gui.x,lightI_gui.y);
   text("K : "+ str(kd),k_gui.x,k_gui.y);
}
  
  
float HalfSphere(PVector sphere_cv, float sphere_r, float x, float y)
{
  float sq = (float)Math.sqrt(sphere_r*sphere_r-(x-sphere_cv.x)*(x-sphere_cv.x)-(y-sphere_cv.y)*(y-sphere_cv.y));
  return sq+sphere_cv.z;  
}

boolean PointInCircle(PVector center, float radius, float x, float y)
{
  return PointInCircle(center,radius,new PVector(x,y));
}

boolean PointInCircle(PVector center, float radius, PVector v)
{
  float dx = v.x-center.x;
  float dy = v.y-center.y;
  if(dx*dx+dy*dy<radius*radius){return true;}
  return false;
}
  
float Dot3(PVector v1, PVector v2)
{
    return (v1.x * v2.x + v1.y * v2.y + v1.z * v2.z);
}  

PVector _press_pos;
PVector _release_pos;
PVector _pprev;
PVector _prev;
PVector _current;
PVector _dv = new PVector(0,0);
int current_vertex_index = -1;

void mousePressed()
{
  _press_pos = new PVector(mouseX,mouseY);
  _pprev = new PVector(mouseX,mouseY);
  _prev = new PVector(mouseX,mouseY);
  _current = new PVector(mouseX,mouseY); 
  
  //Vertices
  for(int i = 0; i<this.Vertices.size(); i++)
  {
    if(PointInCircle(this.Vertices.get(i),this.Vertices_r,new PVector(mouseX,mouseY)))
    {
       current_vertex_index = i;        
    }
  }    
}

void mouseDragged()
{
  if(_current==null){return;}
  if(_prev!=null){_pprev = _prev.copy();}
  
  _prev = _current.copy();
  _current.x = mouseX;
  _current.y = mouseY;  
  _dv = PVector.sub(_current,_prev);
  
  //Vertices
  if(0<=current_vertex_index)
  {
    for(int i = 0; i<this.Vertices.size(); i++)
    {
      this.Vertices.get(current_vertex_index).x = mouseX;
      this.Vertices.get(current_vertex_index).y = mouseY;    
    }    
  }   
}

void mouseReleased()
{
  _release_pos = new PVector(mouseX,mouseY);
  
  _pprev = null;
  _prev = null;
  _current = null;  
  current_vertex_index=-1; 
}


光源の強さ
拡散反射係数
光源の位置のz座標

は、GUIの位置のy座標だけ取得しています。
GUIの位置が画面上の方にあると0
画面下の方にあると1だったり255だったりの最大値を成します。

グリッドの分割数を1000にしたもの。ただしこんなことをするくらいならピクセル単位で走査および操作した方が格段に速いです。また、GPUと連携したシェーダーが多分最速です。


ベクトルのなんちゃらかんちゃらはこちら

ピクセル単位でなんちゃらかんちゃらはこちら


この記事が気に入ったらサポートをしてみませんか?