Realistické zobrazování prostorových objektů

 

K tomu, aby objekt znázorňovaný na obrazovce počítače působil realisticky, je třeba vyřešit několik problémů. První již vyřešen máme:

 

a) volba vhodného promítání na výstupní zařízení. Pro tyto účely je zcela nevhodná kosoúhlá axonometrie. Není vhodné ani promítání na kulovou a válcovou plochu (to známe z uměleckých fotografií pořízených objektivy s velmi širokým zorným úhlem). Nejjednodušším vhodným promítáním je kolmá axonometrie. Hodí se ke zobrazování objektů, které pozorujeme ve velmi malých zorných úhlech. Tyto objekty musí být tedy buď velmi malé, nebo velmi vzdálené od pozorovatele. Chceme-li nějakým způsobem znázornit velikost objektu, je vhodnější lineární perspektiva. Její střed bychom však měli volit tak, abychom nepřekračovali zorný úhel 40o, neboť bychom se zase ocitli spíše v oblastech širokoúhlé fotografie.

b) viditelnost. Je třeba vymyslet algoritmus, který zobrazí jen viditelné části objektu, tj. ty části, jejichž promítací paprsek není na cestě k pozorovateli přerušen.

c) optické vlastnosti povrchu objektu. Vzhled povrchu reálného předmětu závisí na řadě jeho fyzikálních vlastností: na barvě povrchu, na procentu odraženého a pohlceného světla (tj. zda je předmět lesklý či matný), na hladkosti povrchu, indexu lomu atd. Čím více těchto vlastností algoritmus postihuje, tím věrohodněji bude objekt působit.

d) vržené stíny a odlesky. Jeden zobrazovaný objekt má vliv na druhý. Vrhá stíny, lesklý předmět odráží světlo, které může dopadnout na jiný objekt nebo na podložku atd.

 

Vzhledem k omezenému rozsahu textu se nelze zabývat všemi optickými jevy, všimneme se jen těch nejdůležitějších.

 

Viditelnost

 

Základním problémem při realistickém zobrazování prostorových útvarů je určit, které části objektu jsou viditelné a které zakryté. Existuje spousta algoritmů, které tuto úlohu řeší. Jeden z nejrozšířenějších je tzv. malířův algoritmus (Painter's algorithm, Priority list). Princip spočívá v přímém vykreslování ploch na obrazovku, a to v pořadí od nejvzdálenějších po nejbližší vzhledem k pozorovateli. Bližší plochy překryjí vzdálenější a viditelnost je tak vyřešena přirozeným způsobem. Při realizaci tohoto algoritmu však narážíme na dva problémy:

 

1. Plochy se mohou překrývat dosti složitým způsobem. Někdy nelze jednoznačně rozhodnout, která plocha má být kreslena dříve a viditelnost je třeba řešit dosti složitými testy. Rozbor těchto situací přesahuje rámec tohoto textu a nebudeme se jimi zabývat.

 

2. Aby později kreslená plocha překryla plochu dříve kreslenou, je třeba každou plochu vyplnit určitou barvou. DELPHI sice umí plochu vyplnit procedurou FloodFill, ta však pro naše účely nedává uspokojivé výsledky. Vyžaduje totiž zadání barvy, která definuje hranici plochy a její vnitřní bod. V něm je právě potíž. Svírá-li totiž promítaná plocha s promítacím paprskem malý úhel, promítne se do úsečky. Procedura FloodFill zřejmě používá při vyplňování semínkového algoritmu, který v těchto případech naprosto selhává. Pro naše potřeby vystačíme s vyplňováním trojúhelníka a vyřešíme ho vlastním algoritmem.

Segmentace plochy z = f(x; y): protože se budeme nyní zabývat plochami vyjádřenými analyticky, stojíme v první řadě před úkolem, jak plochu rozdělit na jednotlivé části, jak ji segmentovat. Předpokládejme spojitou funkci  definovanou na obdélníku . Plochu budeme interpolovat rovinnými segmenty, a to tak, že na osách zvolíme dělení pomocí kroků ,  a těmito kroky cyklujeme přes intervaly ,.

 

Vrcholy segmentu mají pak souřadnice:

 

Segmenty nejsou obecně rovinné, je proto třeba segment sestrojovat jako dva trojúhelníky, např. , .

 

Předpokládejme nejdříve, že trojúhelník již máme promítnutý do roviny a jeho vrcholy převedeny do světových souřadnic. Tyto body jsou tedy typu TPixel, který je deklarován jako

Type TPixel = array [1..2] of Integer:

 

Procedure TDraw3D.FillTriangle2D(X,Y,Z:TPixel;Red,Green,Blue:Byte);

Const TempRed=254;TempGreen=254;TempBlue=254;              

                                                                                           {barevné složky prozatímní hranice}

Var   i,j,h1,h2,Adr,                                                                                    {indexy}

      MinI,MaxI,MinJ,MaxJ:Integer;                {vrcholy opsaného obdélníka}

      BorderLine         :Boolean;           {identifikátor hranice}

      ScanRow           :PByteArray;             {řádek bitmapy}

begin

  Line2D(X,Y,TempRed,TempGreen,TempBlue); Line2D(Y,Z,TempRed,TempGreen,TempBlue);

  Line2D(X,Z,TempRed,TempGreen,TempBlue);          {ohraničení segmentu}

  Min1:=X[1];Max1:=X[1]; Min2:=X[2];Max2:=X[2];

  if Y[1]<Min1 then Min1:=Y[1]  else if Y[1]>Max1 then Max1:=Y[1];

  if Y[2]<Min2 then Min2:=Y[2]

else if Y[2]>Max2 then Max2:=Y[2];

  if Z[1]<Min1 then Min1:=Z[1]

else if Z[1]>Max1 then Max1:=Z[1];

  if Z[2]<Min2 then Min2:=Z[2]

else if Z[2]>Max2 then Max2:=Z[2];

  For j:=Min2 to Max2 do

  begin

    ScanRow:=Image.Picture.Bitmap.ScanLine[j]; i:=Pred(Min1);

    Repeat                            {postup k hranici segmentu zleva}

    inc(i);Adr:=3*i;

    BorderLine:=(ScanRow[Adr]=TempBlue)

    and (ScanRow[succ(Adr)]=TempGreen) and

        ScanRow[succ(succ(Adr))]=TempRed);

    Until (i=Max1) or BorderLine;

    if BorderLine

     then begin

        h1:=i; i:=succ(Max1);

                                                                {nastavení levého krajního bodu vyplňované úsečky}

        Repeat                       {postup k hranici segmentu zprava}

         dec(i);Adr:=3*i;

         BorderLine:=(ScanRow[Adr]=TempBlue)

     and (ScanRow[succ(Adr)]=TempGreen) and

         (ScanRow[succ(succ(Adr))]=TempRed)

      Until (i=h1) or BorderLine;

        h2:=i;                       {nastavení pravého krajního bodu}

        For i:=h1 to h2 do                                 

        begin                                  {sestrojení úsečky}

         Adr:=3*i;

         ScanRow[Adr]:=Blue;

         ScanRow[succ(Adr)]:=Green;

         ScanRow[succ(succ(Adr))]:=Red;

        end;

       end;

    end;

 end;  

 

Pomocí této procedury pak sestrojíme trojúhelník v prostoru. Podle zvoleného typu promítání promítneme vrcholy do průmětny (procedura Projection), průměty převedeme do světových souřadnic a použijeme FillTriangle2D.

procedure FillTriangle(A,B,C:T3DPoint;Red,Green,Blue :byte);

 var pX,pY,pZ        :T2DPoint;

     X,Y,Z           :TPixel;

 begin

  Projection(A,pX);Projection(B,pY);Projection(C,pZ);

  X[1]:=XCoor(pX[1]);X[2]:=YCoor(pX[2]);

  Y[1]:=XCoor(pY[1]);Y[2]:=YCoor(pY[2]);

  Z[1]:=XCoor(pZ[1]);Z[2]:=YCoor(pZ[2]);

  FillTriangle2D(X,Y,Z,Red,Green,Blue);

 end;

 

Pro případné rozlišení rubu a líce plochy (pokud tyto strany budeme chtít sestrojovat různými barvami) si připravíme následující procedury a funkce:

 

Procedure SetVector(X,Y:T3DPoint;var v:TVector);   

{nastavení vektoru z krajních bodů}

 begin

  v[1]:=Y[1]-X[1];v[2]:=Y[2]-X[2];v[3]:=Y[3]-X[3];

 end;

 

Procedure VectorProduct(r,s:TVector;var t:TVector);  

{vektorový součin vektorů}

 begin

  t[1]:= r[2]*s[3]-s[2]*r[3];  t[2]:=-r[1]*s[3]+s[1]*r[3]; t[3]:= r[1]*s[2]-s[1]*r[2];

 end;

 

function CosAngle(u,v:TVector):Double;   

{kosinus úhlu dvou vektorů}

 begin

CosAngle:=(u[1]*v[1]+u[2]*v[2]+u[3]*v[3])/Sqrt(u[1]*u[1]+u[2]*u[2]+u[3]*u[3])

     /Sqrt(v[1]*v[1]+v[2]*v[2]+v[3]*v[3])

 end;

 

Při konstrukci plochy zadané rovnicí  není třeba zjišťovat vzdálenosti jednotlivých segmentů od pozorovatele, stačí na obou osách postupovat vhodným směrem. V případě, že se pozorovatel nachází v I. oktantu (viz obrázek), je třeba postupovat souhlasně s orientací obou os:

 

 

begin

  hx:=(x2-x1)/CountOfSegments;hy:=(y2-y1)/CountOfSegments;

  y:=y1;

  Repeat

   x:=x1;

   Repeat

    A[1]:=x;   A[2]:=y;   A[3]:=f(x,y);

    B[1]:=x+hx;B[2]:=y;   B[3]:=f(x+hx,y);

    C[1]:=x+hx;C[2]:=y+hy;C[3]:=f(x+hx,y+hy);D[1]:=x;

    D[2]:=y+hy;D[3]:=f(x,y+hy);

    FillTriangle(A,B,C,Red,Green,Blue);

    FillTriangle(C,D,A,Red,Green,Blue);

    x:=x+hx;

    until x>x2;

   y:=y+hy;

until y>y2;

end;

 

 

Poznámky:

 

1. Procedura FillTriangle2D používá při vyplňování přístup do obrazových řádků bitmapy, nikoli matici Pixels objektu Canvas. Vyplňování se tak podstatně urychlí.

 

2. Výše uvedená sekvence kódu nerozlišuje rub a líc plochy. Abychom dostali výstup analogický připojenému obrázku, je třeba tyto strany rozlišit a každou z nich vyplňovat jinou barvou.

 

Červeně uvedené procedury FillTriangle je pak třeba nahradit následující sekvencí:

 

SetVector(B,C,u);SetVector(B,A,v);

VectorProduct(u,v,w);                   {w je normála segmentu ABC}

if CosAngle(w,MajorRay)>0

{je-li úhel normály segmentu a směru pohledu kladný}

   then begin                                                                              {vyplňuj barvou pro líc}

         FillTriangle(A,B,C,RightRed,RightGreen,RightBlue);

         Surf1:=Right;

        end

  else begin                            {jinak vyplňuj barvou pro rub}

         FillTriangle(A,B,C,WrongRed,WrongGreen,WrongBlue);

         Surf1:=Wrong;

       end;

SetVector(D,A,u);SetVector(D,C,v);

VectorProduct(u,v,w);                                                      {totéž pro segment CDA}

if CosAngle(u,w)>0

     then begin

           FillTriangle(C,D,A,RightRed,RightGreen,RightBlue);

           Surf2:=Right;

          end

     else begin

           FillTriangle(C,D,A,WrongRed,WrongGreen,WrongBlue);

           Surf2:=Wrong;

          end;

Line(A,B,0,0,0);Line(B,C,0,0,0);

Line(C,D,0,0,0);Line(D,A,0,0,0);           {obvod segmentu ABCD}

if Surf1<>Surf2

   then Line(A,C,0,0,0); {jsou-li trojúhelníky vidět z různých stran, sestroj i AC}

 

Příklad 1.: Řeší sestrojení plochy  pomocí výše uvedených algoritmů. Na obrázku výše je výstup pro funkci  definované na obdélníku .

 

Zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Je-li plocha zadaná parametrickými rovnicemi   kde    , je situace při konstrukci segmentů je zcela analogická. Pomocí kroků  resp.  zvolíme dělení intervalů a těmito kroky opět cyklujeme přes intervaly ; . Vrcholy jednotlivých segmentů mají tentokrát souřadnice:

 

 

Můžeme beze změny použít výše popsanou proceduru DrawSegment, nelze však „ošidit“ určování pořadí konstrukce segmentů podle vzdálenosti od pozorovatele tak, jak se nám to poštěstilo v předchozím případě. Chceme-li zajistit konstrukci segmentů plochy v pořadí daném jejich vzdáleností od pozorovatele, můžeme postupovat vpodstatě dvojím způsobem:

 

a) Algoritmus, který bychom mohli označit jako „postup ve vrstvách“. Spočívá v tom, že se k pozorovateli blížíme v tenkých vrstvách a v každé vrstvě jsou vykresleny pouze segmenty, které v ní leží. Pro vykreslení či nevykreslení segmentu bude rozhodující vzdálenost jednoho z jeho vrcholů od roviny kolmé ke směru pohledu. Nejjednodušší je volit rovinu procházející počátkem. Je-li její normálový vektor , pak vzdálenost bodu  od této roviny je . Je-li vektor v jednotkový, je jmenovatel tohoto zlomku roven jedné, vynecháme-li navíc absolutní hodnotu, pak znaménko této „vzdálenosti“ navíc říká, zda bod  je před nebo za touto rovinou vzhledem k pozorovateli.

 

 

begin

 hr:=(r2-r1)/CountOfSegments; hs:=(s2-s1)/CountOfSegments;

 Min:=1e6;Max:=-1e6; r:=r1;

Repeat                      {určení minimální a maximální potřebné vzdálenosti od pozorovatele}

 s:=s1;

 Repeat

   A[1]:=Fi(r,s);A[2]:=Psi(r,s);A[3]:=Tau(r,s);

   Distance:=Draw3D.MajorRay[1]*A[1]+Draw3D.MajorRay[2]*A[2] +Draw3D.MajorRay[3]*A[3];

 if Min>Distance then Min:=Distance

                 else if Max<Distance then Max:=Distance;

   s:=s+hs;

 until s>s2;

 r:=r+hr;

Until r>r2;

Layer:=Min;                                                                       {Nastavení zadní krajní polohy}

 While Layer<Max do                                                         {Dokud neprojdeš celý pás}

  begin

    r:=r1;

   Repeat                                      {projdi celou plochu}

    s:=s1;

    Repeat

     A[1]:=Fi(r,s);A[2]:=Psi(r,s);A[3]:=Tau(r,s);          

                                                       {vypočítej vrchol A segmentu a jeho vzdálenost od roviny}

     Distance:=MajorRay[1]*A[1]+MajorRay[2]*A[2]  

               +MajorRay[3]*A[3]-Layer;

     if abs(Distance)<2*hr             {je-li rozdíl vzdálenosti roviny a procházené}

      then begin                          {vrstvy menší než daná hodnota}

       s:=s+hs;B[1]:=Fi(r,s);A[2]:=Psi(r,s);

       A[3]:=Tau(r,s);                                                   {vypočítej ostatní vrcholy}

       r:=r+hr;C[1]:=Fi(r,s);

       C[2]:=Psi(r,s);C[3]:=Tau(r,s);

       s:=s-hs;D[1]:=Fi(r,s);

       D[2]:=Psi(r,s);D[3]:=Tau(r,s);

       r:=r-hr;                                                                                {a segment nakresli}

       FillTriangle(A,B,C,Red,Green,Blue);

       FillTriangle(C,D,A,Red,Green,Blue);

       end;

      s:=s+hs;

    until s>s2;

    r:=r+hr;

  until r>r2;

  Layer:=Layer+2*hr;

 end;

end;

 

(červeně vyznačené procedury lze případně opět doplnit dle předchozího příkladu)

 

Příklad 2.: Zobrazte anuloid s viditelností ve středovém promítání. Příklad je řešen s použitím výše uvedených algoritmů. Vzhledem k tomu, že parametrické rovnice zadává uživatel jako řetězce, je k výpočtu hodnot vrcholů jednotlivých segmentů použita opět funkce Calc (popř. funkce PreCAlc a PostCalc). Jména vstupních řetězců jsou pořadě fa, fb, fc, místo např. C[2]:=Psi(r,s) je třeba psát C[2]:=Calc(fb,ErrorReport). Na připojeném obrázku je výstup pro .

 

Zde najdete                 kompletní zdrojový kód                       a zde                spustitelný kód

 

b) Spočítáme všechny segmenty plochy a uspořádáme je podle vzdálenosti. Tento algoritmus je náročnější na paměť a jeho rychlost závisí na efektivnosti použitého třídícího algoritmu. Celou plochu budeme deklarovat jako pole segmentů, což bude záznam čtyř vrcholů a vzdálenosti od pozorovatele:

 

Type TSegment = record A,B,C,D  :T3DPoint;

                       Dist      :Double;

                end;

var Segment  :array [0..50000] of TSegment;

 

Pro uspořádání pole segmentů podle jejich vzdálenosti od pozorovatele použijeme lze použít např. tzv. bublinovou metodu, která je ovšem pro tyto účely dosti pomalá. Použijeme proto podstatně efektivnější rekurzivní proceduru:

 

procedure sort(Left,Right:LongInt);

var i,j    :LongInt;

    Pivot  :Double;

    Trezor :TSegment;

begin

   i:=Left;j:=Right;

   pivot:=Segment[(Left+Right) div 2].Dist;

   repeat

    while Segment[i].Dist<pivot do inc(i);

    while Segment[j].Dist>pivot do dec(j);

    if i<=j then

       begin

         Trezor:=Segment[i];Segment[i]:=Segment[j];

         Segment[j]:=Trezor;inc(i);dec(j);

       end;

  until i>j;

  if Left<j then sort(Left,j);

  if Right>i then sort(i,Right);

  end;

 

Ve vlastní vykreslovací proceduře nejdříve naplníme pole segmentů:

r:=r1;n:=0;

Repeat

 s:=s1;

 Repeat

   With Segment[n] do

     begin

       A[1]:=Fi(r,s);A[2]:=Psi(r,s);A[3]:=Tau(r,s);

       B[1]:=Fi(r+hr,s);B[2]:=Psi(r+hr,s);B[3]:=Tau(r+hr,s);

       C[1]:=Fi(r+hr,s+hs);C[2]:=Psi(r+hr,s+hs);

                                   C[3]:=Tau(r+hr,s+hs);

       D[1]:=Fi(r,s+hs);D[2]:=Psi(r,s+hs);     

                                   D[3]:=Tau(r,s+hs);

       Dist:=sqr(ObserverPoint[1]-A[1])

       +sqr(ObserverPoint[2]-A[2])+sqr(ObserverPoint[3]-A[3]);

     end;

     inc(n);

     s:=s+hs;

    until s>s2;

    r:=r+hr;

  until r>r2;

  dec(n);Sort(0,n);     {třídění}

  For i:=n downto 0 do  {vykreslení}

 With Segment[n] do

        begin

          FillTriangle(A,B,C,Red,Green,Blue);

          FillTriangle(C,D,A,Red,Green,Blue);

        End;

 

 

 

 

 

 

 

 

 

(opět s možností modifikace vyplňovacích příkazů)

 

Příklad 3.: využívá výše uvedeného algoritmu.

 

Zde najdete      kompletní zdrojový kód                       a zde                           spustitelný kód

 

Tento algoritmus je většinou rychlejší, než algoritmus vrstvový, o čemž se můžeme přesvědčit v následujícím příkladě.

 

Příklad 4.: umožňuje porovnání rychlostí dvou výše uvedených algoritmů.

 

Zde najdete      kompletní zdrojový kód                       a zde                           spustitelný kód

 

 

Příklad 5.: Zobrazte  krychli s viditelností v kolmé axonometrii. Dosud jsme znázorňovali jednobarevné plochy. Tento příklad ukazuje, že u každého segmentu můžeme navíc definovat i jeho vlastní barvu. Toho využijeme především později při nanášení textur. Algoritmus sestrojí tři „pevné“ stěny  krychle a řez kolmý na některou ze souřadných os. Souřadnou osu a bod, kterým řez prochází definuje uživatel. Tento algoritmus sestrojuje krychli tak, že každá její stěna se skládá z  jednobarevných čtvercových segmentů. Na připojeném obrázku je řez kolmý na osu , který ji protíná v bodě . Viditelnost řešena prostým pořadím konstrukce.

 

Zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Tuto úlohu je však možno řešit také podstatně efektivněji, a to tzv. interpolací barvy. Základem algoritmu je interpolace barvy na úsečce v rovině. Zde již nemůžeme použít objekt Canvas, neboť ten interpolovat barvu neumí. Úlohu musíme řešit ve vlastní režii vlastní procedurou - nazvěme ji InterpolLine2D. Jejími parametry jsou krajní pixely a jejich barvy. Souřadnice bodů ležících na úsečce počítáme stejně, jako u jednobarevné úsečky (viz matematické podrobnosti 2. kapitoly). Výpočet barvy ukažme na části kódu sestrojující úsečku s kladnou směrnicí menší než jedna:

 

 

procedure TDraw3D.InterpolLine2D

      (X,Y:TPixel;RedX,GreenX,BlueX,

           RedY,GreenY,BlueY:Byte);

var p,p1,p2,Dx,Dy,i     :Integer;                                    {přírůstky a index}

    M                   :TPixel;                             {bod probíhající úsečku}

    R,G,B,StepR,StepG,StepB:Single; {aktuální bar. složky a jejich přírůstky}

    ScanRow             :PByteArray;  {pole ukazatelů na obrazový řádek}

    Adr                 :Integer;                   {pozice na obrazovém řádku}

Begin

 Dx:=Y[1]-X[1]; Dy:=Y[2]-X[2];                                         {přírůstky na osách}

 M[1]:=X[1];M[2]:=X[2];                                    {počáteční bod a jeho konstrukce}

 ScanRow:=Image.Picture.Bitmap.ScanLine[M[2]];

 Adr:=3*M[1];

 ScanRow[Adr]:=BlueX;

 ScanRow[succ(Adr)]:=GreenX;

 ScanRow[succ(succ(Adr))]:=RedX;

 if (Dx>0) and (Dy>0) and (Dx>Dy)                        {vybraná poloha 0<k<1}

 then begin

        StepR:=(RedY-RedX)/DX;                          {nastavení barevných kroků}

        StepG:=(GreenY-GreenX)/DX;

        StepB:=(BlueY-BlueX)/DX;

        R:=RedX;G:=GreenX;B:=BlueX;                                {počáteční barva}

        p1:=2*Dy;p2:=2*(Dy-Dx);p:=2*Dy-Dx;      {nastavení prediktorů}

        ScanRow:=Image.Picture.Bitmap.ScanLine[M[2]];

       While M[1]<Y[1] do                                                       {postup po úsečce}

       begin

         inc(M[1]);                                    {skok na následující x-ovou souřadnici}

         if p>0 then begin                   {výpočet a nastavení y-ové souřadnice}

                      inc(M[2]);p:=p+p2;

                      ScanRow:=

                      Image.Picture.Bitmap.ScanLine[M[2]];

                     end

               else p:=p+p1;

         Adr:=3*M[1];

         R:=R+StepR;G:=G+StepG;B:=B+StepB; {skok na následující barvu}

         ScanRow[Adr]:=Round(B);                                           {obarvení bodu}

         ScanRow[succ(Adr)]:=Round(G);

         ScanRow[succ(succ(Adr))]:=Round(R);

      end;

   end;

 

Pomocí takto barevně interpolované úsečky můžeme sestrojit barevně interpolovaný trojúhelník. Postup je analogický jako v proceduře FillTriangle s těmito rozdíly: Konstrukce popsané v této proceduře, tj. ohraničení trojúhelníka „prozatímní“ barvou za účelem rozpoznání hranic pro vyplnění a nalezení hranice na jednotlivých obrazových řádcích (proměnné h1, h2) je třeba provádět do pomocného („paměťového“) obrazu (v řešeném příkladu i na připojeném obrázku je označen jako MemoryImage. Hranice sestrojovaného trojúhelníku je totiž konstruována výše uvedenou procedurou InterpolLine2D, nemá konstantní barvu a nebylo by ji tak možno rozeznat. Vodorovné úsečky ohraničené proměnnými h1, h2 jsou rovněž prováděny interpolovanou úsečkou. Takto rastrově vyplňovaný průmět trojúhel-níku je základem procedury

 

FillInterpolTriangle(A,B,C, RedA,GreenA,BlueA, RedB,GreenB,BlueB,

                                                                                            RedC,GreenC,BlueC)

 

s jejíž pomocí snadno sestrojujeme barevně interpolované stěny. Např. řez  krychlí rovnoběžný se stěnou  a na ose Green procházející bodem  vypadá takto:

 

A[1]:=255;A[2]:=150;A[3]:=255; B[1]:=255;B[2]:=150;B[3]:=   0;

C[1]:=  0;C[2]:=150;C[3]:=   0; D[1]:=  0;D[2]:=150;D[3]:=255;

FillInterpolTriangle(A,B,C,

                  Trunc(A[1]),Trunc(A[2]),Trunc(A[3]),

                  Trunc(B[1]),Trunc(B[2]),Trunc(B[3]),

                  Trunc(C[1]),Trunc(C[2]),Trunc(C[3]));

FillInterpolTriangle(C,D,A,

                  Trunc(C[1]),Trunc(C[2]),Trunc(C[3]),

                  Trunc(A[1]),Trunc(A[2]),Trunc(A[3]),

                  Trunc(D[1]),Trunc(D[2]),Trunc(D[3]));

 

Zde najdete      kompletní zdrojový kód                       a zde                           spustitelný kód

 

Plochy určené rovnicí

 

Jak již bylo řečeno, algoritmus konstrukce těchto ploch je trojrozměrným zobecněním algoritmu konstrukce křivek  (viz kpt. 3.4. odstavec Síťová konstrukce) . Zatímco rovinná křivka  byla podmnožinou obdélníka , který jsme rozdělili rovinnou sítí na množinu obdélníků - fyzických pixelů ( domén), plocha  je podmnožinou kvádru , který rozdělíme prostorovou mřížkou na množinu kvádrů - fyzických voxelů ( domén).

 

 

Ve dvojrozměrném případě jsme pracovní plochu rozdělili na obdélníky , kde

 

  ;  ; ;   

 

a zjišťovali jsme, zda křivka  protíná testovaný obdélník. To se stane zřejmě právě tehdy, když alespoň ve dvou jeho vrcholech má funkce  různá znaménka.

 

V trojrozměrném případě máme pracovní objem rozdělen na kvádry , kde

 

;     ;     ;    

; ; ; .

 

Podobně jako ve dvojrozměrném případě ohodnoťme i zde vrcholy kvádru postupně hodnotami ; ; ;...;, a to právě tehdy, když v příslušném vrcholu je , jinak vrcholu přiřaďme nulu. Celý kvádr tak může být ohodnocen hodnotami . Z hlediska naší konstrukce je tedy celkem 256 možností, jak může plocha kvádr protínat. Kvádr na obrázku vlevo je ohodnocen číslem , kvádr vpravo pak číslem . Součet těchto ohodnocení je 255 a je zřejmé, že postup konstrukce bude v obou případech stejný - v obou případech je třeba hledat průsečíky na hranách , , , ,  a nalezený řez pak interpolovat celkem třemi trojúhelníky. Ačkoli je tedy celkový počet případů, které je třeba řešit, dvě stě padesát čtyři (kvádry s ohodnoceními 0 resp.255 plocha neprotíná), program stačí větvit „pouze“ na sto dvacet sedm větví. Hodnoty řezů je opět možno názorně vyjádřit ve dvojkové soustavě, popř. se pokusit o redukci větvení programu pomocí symetrií jednotlivých případů. Je-li např. ohodnocení vyjádřeno jako mocnina dvou (tj. ), znamená to, že  platí právě v jenom vrcholu a řezem je tedy jediný trojúhelník.

 

Vzhledem k tomu, co bylo řečeno výše, dostáváme trojúhelníkový řez rovněž pro . Všech těchto šestnáct případů bychom tak mohli řešit jedinou větví programu. Redukce počtu případů je ovšem možná pouze dalším ne zrovna jednoduchým programováním rozpoznávání symetrií a jejich převodem na jediný případ. Je tedy otázkou, zda by výsledný program byl jednodušší. V každém případě by však byl právě o toto rozpoznávání a tyto převody pomalejší.

 

 

V konkrétní programové realizaci jsou hodnoty funkce  ukládány do trojrozměrného pole Grid. Průsečík plochy s testovanou hranou bychom mohli opět hledat půlením intervalu tak, jako jsme to prováděli v kpt. 3.4. (viz proceduru HorizHalf resp. VertikHalf). Zde bychom museli rozlišovat trojí půlení (ve směrech souřadných os). Poněkud jednodušší a rychlejší postup je nalezení přibližného průsečíku lineární interpolací. Tato interpolace zpracovává body X, Y, kde první tři souřadnice udávají pozici bodu v mřížce (poli Grid), čtvrtá pak funkční hodnotu funkce . Procedura nastavuje souřadnice interpolovaného průsečíku hrany XY s plochou :

 

Procedure Interpolation(X,Y:T3DPoint;var Z:T3DPoint);

begin

 t:= - X[4]/(Y[4]-X[4]);

 Z[1]:=X[1]+t*(Y[1]-X[1]);

 Z[2]:=X[2]+t*(Y[2]-X[2]);

 Z[3]:=X[3]+t*(Y[3]-X[3]);

end;

 

Do control panelu je třeba kromě obvyklých parametrů zadat i pracovní objem pomocí proměnných DefX1,...,DefZ2 a dále počet uzlových bodů na jeho nejdelší hraně. Všechny parametry nutné pro chod programu pak přečte procedura Setting:

 

 Procedure TControl_Panel.Setting(Sender:TObject);

 var Code:Integer;

 begin

   Val(EditDefX1.Text,DefX1,Code);

   Val(EditDefX2.Text,DefX2,Code);:=Number;

   Val(EditDefY1.Text,DefY1,Code);

   Val(EditDefY2.Text,DefY2,Code);:=Number;

   Val(EditDefZ1.Text,DefZ1,Code);

   Val(EditDefZ2.Text,DefZ2,Code);:=Number;

   Val(EditNodalPoints.Text,NoOfNodalPoints,Code);

 end;

 

Vlastní procedura pak vypadá následovně (uvádíme jen fragment):

 

begin

  With Draw3D do

  begin

  ........................ {po obvyklém načtení parametrů a nastavení měřítka}

  Max:=DefX2-DefX1;                              {zjistíme nejdelší hranu pracovního objemu}

  if DefY2-DefY1>Max then Max:=DefY2-DefY1;

  if DefZ2-DefZ1>Max then Max:=DefZ2-DefZ1;

  hx:=Max/(NoOfNodalPoints-1);    {a nastavíme kroky vjednotlivých směrech}

  hy:=hx;hz:=hx;

  z:=DefZ1;k:=0;                                              {napočítáme funkční hodnoty v mřížce}

  Repeat

   x:=DefX1;i:=0;

   Repeat

    y:=DefY1;j:=0;

    Repeat

      Grid[k,i,j]:=f(x,y,z);y+hy;inc(j);

    Until y>DefY2;

    x:=x+hx;inc(i);

  Until x>DefX2;

  z:=z+hz;inc(k);

Until z>DefZ2;

MaxI:=i-2;MaxJ:=j-2; MaxK:=k-2;

PocetSegmentu:=0;                                         {počet segmentů, které je třeba sestrojit}

For k:=0 to MaxK do

 For j:=0 to MaxJ do

   For i:=0 to MaxI do

   begin                                                             {napočítáme vrcholy testovaného kvádru}

   A[1]:= i;A[2]:=j;A[3]:=k;A[4]:=Grid[k,i,j];

   B[1]:=succ(i);B[2]:=j;B[3]:=k;B[4]:=Grid[k,succ(i),j];

   C[1]:=succ(i);C[2]:=succ(j);C[3]:=k;C[4]:=Grid[k,succ(i),succ(j)];

   D[1]:=i;D[2]:=succ(j);D[3]:=k;D[4]:=Grid[k,i,succ(j)];

   Ac[1]:=i;Ac[2]:=j;Ac[3]:=succ(k); Ac[4]:=Grid[succ(k),i,j];

   Bc[1]:=succ(i);Bc[2]:=j;Bc[3]:=succ(k);Bc[4]:=Grid[succ(k),succ(i),j];

   Cc[1]:=succ(i);Cc[2]:=succ(j);Cc[3]:=succ(k);Cc[4]:=Grid[succ(k),succ(i),succ(j)];

   Dc[1]:=i;Dc[2]:=succ(j);Dc[3]:=succ(k); Dc[4]:=Grid[succ(k),i,succ(j)];

   Citac:=0;      {pomocí této proměnné zjistíme „hodnotu“ testovaného kvádru}

   if A[4]>0 then Citac:=Citac+1;

   if B[4]>0 then Citac:=Citac+2;

   if C[4]>0 then Citac:=Citac+4;

   if D[4]>0 then Citac:=Citac+8;

   if Ac[4] >0 then Citac:=Citac+16;

   if Bc[4]>0 then Citac:=Citac+32;

   if Cc[4]>0 then Citac:=Citac+64;

  if Dc[4]>0 then Citac:=Citac+128;

   Case Citac of

    1,254:begin                  {znaménko funkce se liší pouze v bodě A}

          inc(PocetSegmentu); {je třeba nastavit jeden trojúhelníkový segment}

          With Segment[PocetSegmentu] do

          begin {Vrcholy segmentu nastavíme jako průsečíky na hranách AB, AD, AA'}

           Interpolation(A,B,SegmA);

           Interpolation(A,D,SegmB);

           Interpolation(A,Ac,SegmC);

           end;

          end;

      .......................................

59 ,196:begin                      {případy dle výše uvedených obrázků}

       inc(PocetSegmentu);    {je třeba nastavit tři trojúhelníkové segmenty}

       With Segment[PocetSegmentu] do

       begin

         Interpolation(Ac,Dc,SegmA);

         Interpolation(D,Dc,SegmB);

         Interpolation(Bc,Cc,SegmC);

       end;

       inc(PocetSegmentu);

       With Segment[PocetSegmentu] do

       begin           

         Interpolation(C,D,SegmA);

         Interpolation(D,Dc,SegmB);

         Interpolation(Bc,Cc,SegmC);

       end;

       inc(PocetSegmentu);

       With Segment[PocetSegmentu] do

       begin

         Interpolation(C,D,SegmA);

         Interpolation(B,C,SegmB);

         Interpolation(Bc,Cc,SegmC);

       end;

     end;

      .......................................

  end;           { Case Citac of}

 

Podobně jako v př. 3. předchozí kapitoly následuje třídění segmentů podle jejich vzdálenosti od pozorovatele algoritmem Quick Sort a jejich vykreslení.

 

Příklad 1: Řeší konstrukci ploch  výše uvedeným algoritmem. Na připojeném obrázku si můžeme prohlédnout plochu  pro

 

Zde najdete kompletní    zdrojový kód               a zde            spustitelný kód

 

Vlastní stín

 

Dalším krokem ke zlepšení vzhledu zobrazovaného objektu je stínování. Reálné předměty jsou vyrobeny z různého materiálu a jejich povrchy mají různý vzhled. Hovoříme-li o vzhledu povrchu, říkáme, že je červený, lesklý, drsný, průhledný atd. Tyto vlastnosti se vztahují vesměs k optickým vlastnostem povrchu, popř. celého tělesa. Dopadne-li světlo na povrch tělesa, je částečně pohlceno a částečně odraženo. Předpokládejme nejjednodušší případ, kdy je těleso přímo osvětleno jedním plošným zdrojem bílého světla. Dopadá-li navíc světelný paprsek kolmo na plochu, je barva určena schopností plochy odrážet jednotlivé vlnové délky dopadajícího světla. V modelu  můžeme tuto barvu definovat velikostí červené, zelené a modré složky. Dopadá-li světlo pod jiným úhlem, klesá i světelný tok, který na daný plošný element dopadá. Tok dopadající na plošnou jednotku segmentu je pak přímo úměrný velikosti průmětu segmentu do roviny kolmé k dopadajícímu světelnému paprsku (viz obrázek).

 

V našem grafickém modelu máme pro každou složku k dispozici 256 hodnot. Definujeme-li barvu hodnotami R=191; G=127; B=191; znamená to, že segment odráží 75% dopadající červené a modré a 50% zelené - výsledná barva je fialová. Složky odraženého světla jsou pak Rcosw, Gcosw, Bcosw. V programovém zpracování je tedy třeba počítat normálu každého segmentu a její úhel se směrem dopadajícího světla. Použijeme přitom známých vzorečků (viz procedury SetVector, VectorProduct a CosAngle v předchozí kapitole)

 

Konstantní stínování: Stínovaný segment  opět rozdělíme na dva trojúhelníky, Procedurou CosAngle určíme úhel normály a dopadajícího světla pro každý z nich a barvu výplně určíme vynásobením barevných složek pro líc resp. rub právě tímto kosinem:

 

 

Direct:=CosAngle(u,w);

if Direct>0

then begin

        Red:=Trunc(Direct*RightRed);

        Green:=Trunc(Direct*RightGreen);

        Blue:=Trunc(Direct*RightBlue);

     end

else begin

        Red:=Trunc(-Direct*WrongRed);

        Green:=Trunc(-Direct*WrongGreen);

        Blue:=Trunc(-Direct*WrongBlue);

     end;    

FillTriangle(A,B,C,Red,Green,Blue);

FillTriangle(C,D,A,Red,Green,Blue);

 

Uvedená metoda je známa jako konstantní stínování, neboť celý rovinný segment vyplní toutéž barvou. To je sice nejjednodušší, při použití velkých segmentů však zdůrazňuje, že „oblý“ povrch tělesa je aproximován rovinnými segmenty. Tomu se dá předejít buď volbou jiné stínovací metody, nebo zmenšováním segmentů.

 

Příklad 1.: Sestavte program pro zobrazování grafů spojitých funkcí  metodou konstantního stínování. Celý příklad je zpracován analogicky jako př. 1. předchozí kapitoly a modifikován dle výše uvedených sekvencí. Příklad umožňuje uživateli stejně jako v předchozích případech zadávat rovnici plochy, parametry perspektivního promítání, barvu pro líc a rub plochy a dále počet segmentů a tím také jejich velikost. Na přiloženém obrázku je výstupy z programu, kdy úpočet zadaných segmentů byl v prvním případě 15, ve druhém 40 a ve třetím 160.

 

Zde najdete                 kompletní zdrojový kód                       a zde                spustitelný kód

 

 

Příklad 2.: Sestavte program pro zobrazování parametricky zadaných ploch metodou konstantního stínování. Celý příklad je opět zpracován analogicky jako př. 2. předchozí kapitoly.

 

Zde najdete                 kompletní zdrojový kód                       a zde                spustitelný kód

 

Drsnost povrchu: reálné objekty nemají nikdy dokonale hladký povrch. Větší či menší drsnost povrchu plochy se projeví větší či menší náhodností odrazu dopadajícího paprsku. To lze zohlednit větším či menším rozptylem barvy při vykreslování jednotlivých segmentů procedurou DrawSegment. Poslední dva příkazy v proceduře FillTriangle - MoveTo(h1,i); LineTo(h2,i); které vykreslují vodorovnou úsečku konstantní barvou, je třeba nahradit postupem po jednotlivých pixelech. V proceduře je třeba deklarovat proměnné Red, Green, Blue typu Integer. Pro každý pixel nastavíme tyto hodnoty na uživatelem definovanou barvu a každou složku rozmažeme proměnnou RoughPixel, kterou nastavujeme randomem vynásobeným koeficientem Rough-ness, který pro drsnost zadal uživatel. Takto rozmazanou hodnotu musíme ještě ošetřit proti přetečení:

 

for j:=h1 to h2 do

  begin

    Shade:=2*Round((Random-0.5)*Roughness);

                                 {nastavení rozptylu světla pro daný pixel}

    if Red+Shade>255 then R:=255

    else if Red+Shade<0 then R:=0

                                                                           {ošetření proti přetečení a rozmazání červené}

                        else R:=Red+Tone;

  ...................                                                           {totéž pro Green, Blue}

  Draw3D.Image1.Canvas.Pixels[j,i]:=SetColor(R,G,B);

 end;

 

Je-li hodnota Shade pro každou barevnou složku stejná, nemění se barevný odstín drsné plochy (viz pravá část výstupu). Měníme-li tuto hodnotu, měníme bod po bodu také barevný odstín, čímž plocha získává i „perleťový“ vzhled (viz levá část výstupu).

 

Na připojeném obrázku vidíme část anuloidu, zhotoveného z drsného materiálu.

 

 

 

Příklad 3 - studie drsných povrchů ploch zadaných explicitně

 

Zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Příklad 4 - studie drsných povrchů ploch zadaných parametricky

 

Zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Osvětlení a lesk povrchu: Na začátku kapitoly jsme konstatovali, že barva segmentu, kterou vnímá pozorovatel, závisí na kosinu úhlu, který svírá jeho normála se směrem dopadajícího světla (tato hodnota je vyčíslována funkcí CosAngle). Jestliže barevné složky přepočítáváme jinými funkcemi, můžeme simulovat rùznou intenzitu osvětlení a lesk plochy.

 

Příklad 5, 6: Zde jsou zprogramovány studie lesku a osvětlení explicitně a parametricky zadaných ploch. Pro úpravu intenzity barvy jsou použity dvě funkce. Parametrem Osvětlení (v programu proměnná Light) měníme exponent  funkce . Ten nemusí být celočíselný, nebot´ funkce je vyčíslována logaritmicky - tosv:=exp(Light*ln(tosv)). Pro změnu lesku je použita exponenciální funkce 1-exp(-1/sqr(Shine*tosv-pi/2)), kde parametr Shine zadává uživatel. Na připojeném obrázku vidíme výstup, kde můžeme srovnat různě osvětlené a různě matné části kulové plochy.

 

 

 

Studie lesku a osvětlení pro funkce dvou proměnných

 

kompletní zdrojový kód                           spustitelný kód

 

Plochy zadané parametrickými rovnicemi

 

kompletní zdrojový kód                           spustitelný kód

 

Interpolace stínu: Jak již bylo řečeno, konstantní stínování nežádoucím způsobem zvýrazňuje hrany. Jedním ze způsobů, jak tomu předejít, je interpolace stínu. Interpolujeme-li sestrojovanou plochu jednotlivými segmenty tak, jak bylo popsáno výše, pak tato plocha prochází každým vrcholem libovolného segmentu. Deklarujme v programu segmenty plochy takto:

 

 

Type TSegment = record      A,     B,     C,     D:T3DPoint;

                       ShadeA,ShadeB,ShadeC,ShadeD,

                       Dist                       :Double;end;

var Segment :array [0..50000] of TSegment;

 

Při interpolaci stínu stojíme v první řadě před úkolem určit přibližně směr normály hledané plochy ve společném vrcholu čtyř interpolujících segmentů. Interpolující segment  není obecně rovinný a lze v něm získat celkem čtyři normály - normálové vektory rovin trojúhelníků , , , . Označme společný vrchol čtyř sousedních segmentů . Segmenty očíslujme a jejich vrcholy popišme dle připojeného obrázku. Bod  pak inciduje s body Segment[1].C, Segment[2].D, Segment[3].B a Segment[4].A. Konstrukce normály sestrojované plochy v bodě  je pak zřejmá z následující sekvence:

 

With Segment[1] do

begin

 SetVector(C,B,u);SetVector(D,C,v);

 VectorProduct(u,v,w);

 Normal:=w;

end;

With Segment[2] do

begin

 SetVector(D,A,u);SetVector(D,C,v);

 VectorProduct(u,v,w);

 Normal[1]:= Normal[1]+w[1];

 Normal[2]:= Normal[2]+w[2];

 Normal[3]:= Normal[3]+w[3];

end;

With Segment[3] do

begin

 SetVector(B,A,u);SetVector(B,C,v); VectorProduct(u,v,w);

 Normal[1]:= Normal[1]+w[1];

 Normal[2]:= Normal[2]+w[2];

 Normal[3]:= Normal[3]+w[3];

end;

With Segment[4] do

begin

 SetVector(A,B,u);SetVector(A,D,v); VectorProduct(u,v,w);

 Normal[1]:= Normal[1]+w[1];

 Normal[2]:= Normal[2]+w[2];

 Normal[3]:= Normal[3]+w[3];

end;

 

Vrcholům jednotlivých segmentů, které incidují s bodem  pak přiřadíme zastínění dle kosinu úhlu, který svírá tato normála se světelným paprskem:

 

Segment[1].ShadeC:=CosAngle(Normal,DirectOfLight);

Segment[2].ShadeD:=Segment[1].ShadeC; Segment[3].ShadeB:=Segment[1].ShadeC; Segment[4].ShadeA:=Segment[1].ShadeC;

 

 

 

 

 

Plocha na připojeném obrázku je sestrojena pomocí velkých segmentů. Levá část konstantním stínováním, pravá pak pomocí interpolace stínu.

 

Příklad 7, 8: Zde je zprogramována interpolace stínu pro

plochy zadané explicitně:          zde najdete kompletní zdrojový kód

a zde           spustitelný kód

a parametricky:                        zde najdete kompletní zdrojový kód

a zde          spustitelný kód

 

Pro plochy zadané rovnicí  je výpočet normál potřebných pro interpolaci stínu velmi jednoduchý, neboť normálu této plochy lze s dostatečnou přesností nahradit gradientem funkce .

 

Příklad 9: Zde je zprogramována interpolace stínu pro plochu .

 

Zde najdete   kompletní zdrojový kód  a zde   spustitelný kód

 

 

 

Nanášení textur

 

Povrch reálných předmětů má také málokdy konstantní barvu. Různobarevnost plochy vyjádříme nejlépe nanesním textury. Texturou rozumíme funkci, která přiřazuje bodům roviny hodnotu modulované veličiny, v našem případě barvy: , kde  pro spojitý a  pro diskrétní případ. Aplikaci této textury na povrch tělesa provedeme definováním tzv. mapovací funkce, která každému bodu z definičního oboru textury přiřadí bod  na povrchu  tělesa. Barva tohoto bodu je pak definována hodnotou textury . Definiční obor textury se skládá z fyzických pixelů, tj. logických čtverců o straně 1 (ve světových souřadnicích). Mapovací funkcí přiřadíme každému pixelu textury segment  námi sestrojované plochy. Měla by měla být prostá, protože v programové realizaci potřebujeme většinou obrácený postup – dle parametrizace texturované plochy procházíme segment po segmentu a každému z nich přiřazujeme barvu z textury. Mapovací funkce není určena jednoznačně. Tvoří-li povrch tělesa jediná analytická plocha, je nejjednoduší volit jako mapovací funkci přímo parametrizaci plochy. Texturu můžeme definovat buď matematickým předpisem (nejčastěji u jednoduchých pravidelných textur), nebo tabulkou hodnot (nejlépe ve formě obrazu).

 

 

Příklad 1: Aplikujme šachovnicovou texturu na parametricky zadanou plochu: Mapování zde provádíme pomocí pole proměnných typu TSegment:

 

TSegment = record  v,r,s    :double; Vert,Horiz:Integer;  end;

 

což jsou segmenty, ze kterých je plocha sestrojena. Segment je určen hodnotami parametrů r, s pro vrchol A, hodnota v udává jeho vzdálenost od pozorovatele (pro řešení viditelnosti. Samotná plocha je pak deklarována jako pole segmentů:  

 

 

if (odd(Segment[Index].Vert div GreatOfSquare) and

    odd(Segment[Index].Horiz div GreatOfSquare)) or

   (not odd(Segment[Index].Vert div GreatOfSquare) and

 not odd(Segment[Index].Horiz div GreatOfSquare))

   then begin

          RedSegm:=RightRed;

          GreenSegm:=RightGreen;

          BlueSegm:=RightBlue;

        end

   else begin

       RedSegm:=WrongRed;

       GreenSegm:=WrongGreen;

       BlueSegm:=WrongBlue;

        end;

 

 

Složky barev čtverců jsou definovány proměnnými Right- resp Wrong-, rub plochy je obarven negativem.

 

zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Nanášením obecných nepravidelných textur můžeme poměrně věrně imitovat materiál, ze kterého je těleso či plocha zhotovena. Velmi zajímavé výsledky můžeme získat také tehdy, použijeme-li jako texturu fotografii, či jiný zajímavý obrázek.

 

Příklad. 2: Zde je aplikována obecná textura na anuloid. V prvním případě je imitováno dřevo. Textura je definována pomocí obrazu. Největším problémem je zde vytvoření „věrohodné“ předlohy. Ta může být vytvořena uměle pomocí výtvarných technik nebo sejmutím skutečného povrchu daného materiálu. Ve druhém případě je textura získána fraktálními technikami, o kterých pojednává následující kapitola (zde se jedná o detail tzv. Mandelbrotovy množiny). Segmentace plochy je vždy volena podle textury, a to tak, že každý bod definičního oboru textury odpovídá právě jednomu segmentu plochy. Mapovací funkce je opět dána přímo parametrizací plochy.

 

 

 

zde najdete      kompletní zdrojový kód           a zde                spustitelný kód

 

Nanášení textur však neslouží jen účelům návrhářským či uměleckým, ale má také významná využití technická. Významné využití poskytuje kartografie. Kartografové řeší většinou problém opačný - totiž rozvinutí zemského povrchu do roviny. Z grafického hlediska je naše Země (přibližně) koule, na které je nanesena textura. Rovinná mapa je vlastně textura, kterou máme nanést na kouli. Máme-li obdélníkovou mapu světa, můžeme si s našimi současnými znalostmi vyrobit zeměkouli. Princip je stejný jako v př. 2. Nanášení textury probíhá v cyklu. Výsledkem jednoho jeho průběhu je jeden obrázek, který uložíme jako jedno okénko budoucího filmu. Součástí názvu by mělo být i číslo snímku, které pak usnadní cyklus čtení. Jednotlivé snímky se liší otočením o úhel 360/n ( u koule toho můžeme docílit pouhým posunutím nanášené textury). Takto vytvořený film pak promítáme jiným programem, který v cyklu pro n=0..51 provede jednu otáčku zeměkoule tím, že vždy zjistí, který obrázek má být promítnut, pošle ho na kreslící plochu a okamžitě přechází na další snímek. Vytvoření jednotlivých snímků je sice časově náročné, „film“ zabírá hodně místa na disku, přesto si ale myslím, že výsledek stojí za to.

 

 

 

Zde najdete                 kompletní zdrojový kód                       a zde                spustitelný kód.

 

Popsané algoritmy nanášení textury jsou sice poměrně jednoduché, zato však bohužel dosti pomalé. Náš program zpomaluje především vyčíslování rovnic z řetězce zadaného uživatelem. Reprezentace fyzických pixelů textury segmenty plochy vyžaduje poměrně velký počet segmentů, poměrně pomalé je také jejich vyplňování, použitý Painter's algorithm pro řešení viditelnosti bohužel také není z nejrychlejších (velká část obrazu se počítá a kreslí zbytečně, protože bude později překreslena). Relativní jednoduchost použitých algoritmů je prostě zaplacena pomalejším provedením. Na Control panelu je třeba nejdříve nahrát zvolenou texturu tlačítkem Loading, poté spustit vlastní nanášení tlačítlem Start.

 

Pro speciální plochy (např. kulová plocha, válcová plocha aj.) lze nanášení textury podstatně urychlit postupem, který je popsán v matematických podrobnostech

 

Vržený stín

 

Kromě vlastního stínu, který vzniká v důsledku různého úhlu normál segmentů plochy a dopadajícího světla, existuje stín vržený. Nachází-li se těleso mezi zdrojem světla a zbytkem scény, pak tuto část scény zastíní. Jednoduché řešení vržených stínů jsme již předvedli pomocí osové afinity. Toto řešení umožňuje zadat tvar stínu, aniž bychom cokoli věděli o zdroji světla, díky jemuž stín vzniká. Situace však většinou bývá opačná: známe zdroj světla a z něj potřebujeme odvodit stín. Řešení vržených stínů může být dosti komplikované (těleso může např. zastiňovat i část sebe sama). Podíváme se na nejjednodušší případ, kdy těleso osvětlené jedním bodovým zdrojem vrhá stín na vodorovné pozadí. Světelný paprsek je jednoznačně určen směrovým vektorem , kde  je světelný zdroj (v programu označen jako LightSource) a  je bod na povrchu tělesa. Ten pak vrhne stín  na další součást scény, která stojí světelnému paprsku v cestě. Jak již bylo řečeno, budeme předpokládat rovinné vodorovné pozadí. Bod  vypočteme tedy jako průsečík světelného paprsku s rovinou  (v připojené proceduře je tato proměnná pojmenována Depth).

 

Procedure PointShadow(A:T3DPoint;var Ac:T3DPoint);

begin

     With Draw3D do

     begin

         t:=-(LightSource[3]-Depth)/(A[3]-LightSource[3]);

         Ac[1]:=LightSource[1]+(A[1]-LightSource[1])*t;

         Ac[2]:=LightSource[2]+(A[2]-LightSource[2])*t;

         Ac[3]:=-Depth;

     end;

end;

 

 

Plocha je segmentována stejně jako v př. 2 kpt. 8.1. Segment plochy určený vrcholy  (které musí postupně projít procedurou PointShadow) vrhá pak stín určený vrcholy  (které jsme postupně obdrželi jako výstupy z této procedury). Stín budeme sestrojovat pomocí procedury FillTriangle použitím černé barvy na bílé pozadí. Protože tato procedura pracuje s objektem Image, musíme si obsah původního podkladu před stínováním uschovat. Pracujeme-li s maticí Canvas, můžeme deklarovat dvojrozměrné pole prvků typu DWord a matici nazvat např. Ground. Pak ji můžeme naplnit napři takto:

 

  for i:=0 to Image.Width-1 do

    for j:=0 to Image. Height-1 do

         Ground[i,j]:=Image.Canvas.Pixels[i,j];

 

V případě práce s obrazovými řádky je lépe k tomuto účelu využít jako paměti pomocný obraz (v Graph3D je k tomuto účelu k dispozici MemoryImage, který byl již rovněž zmiňován). Je třeba mít deklarovány dvě proměnné jako ukazatele na obrazový řádek, např:

 

var SL, MemSL:PByteArray;

 

Samotné uschování obrazu pak vypadá takto:

 

for j:=0 to Image.Height-1 do

begin

 SL    :=Image.Picture.Bitmap.ScanLine[j];

 SLMem:=MemoryImage.Picture.Bitmap.ScanLine[j];

 for i:=0 to Image.Width-1 do

 begin

   Adr:=3*i;

   SLMem[Adr]:=SL[Adr];

   SLMem[succ(Adr)]:=SL[succ(Adr)];

   SLMem[succ(succ(Adr))]:=SL[succ(succ(Adr))];

   end;

end;

 

Kreslicí plochu Image pak můžeme vymazat a sestrojit do ní černou barvou stín:

 

for i:=0 to NoOfSegments do

 With Segment[i] do

  begin

  PointShadow(A,Ac);PointShadow(B,Bc);PointShadow(C,Cc);PointShadow(D,Dc);

  Draw3D.FillTriangle(Ac,Bc,Cc,0,0,0);Draw3D.FillTriangle(Cc,Dc,Ac,0,0,0);

  end;

 

Dále porovnáváme bod po bodu obrazy Image a MemoryImage. Je-li bod na Image bílý, obarvíme ho původním pozadím, je-li černý, původnímu pozadí poněkud snížíme jas, např. vhodně zvolenou konstantou Brightness z intervalu . Při práci na pozadí (Canvas) můžeme postupovat např. takto:

 

for i:=0 to Image.Width-1 do

 for j:=0 to Image. Height-1 do 

    if Image.Canvas.Pixels[i,j]=clWhite

        then Image.Canvas.Pixels[i,j]:=MemoryImage.Canvas.Pixels[i,j]

        else begin

             With MemoryImage.Canvas do

              begin

               Blue:=Pixels[i,j] div (256*256);

               Green:=(Pixels[i,j-256*256*Blue) div 256;

               Red:=Pixels[i,j]-256*256*Blue-256*Green;

              end;

       Red:=Round(Brightness*Red);

              Green:=Round(Brightness*Green);

              Blue:=Round(Brightness*Blue);

              Image.Canvas.Pixels[i,j]:=

                      Red+256*Green+256*256*Blue;

          end;

 

Práce v obrazových řádcích:

 

for j:=0 to Image.Height-1 do

begin

 SL    :=Image.Picture.Bitmap.ScanLine[j];

 SLMem:=MemoryImage.Picture.Bitmap.ScanLine[j];

 for i:=0 to Image.Width-1 do

 begin

   Adr:=3*i;

   if (SL[Adr]=255) and (SL[succ(Adr)]=255) and

                       (SL[succ(succ(Adr))]=255)

   then begin

        SL[Adr]:=SLMem[Adr];

        SL[succ(Adr)]:=SLMem[succ(Adr)];

        SL[succ(succ(Adr))]:=SLMem[succ(succ(Adr))];

       end

   else begin

        SL[Adr]:=Round(Brightness*SLMem[Adr]);

        SL[succ(Adr)]:=Round(Brightness*SLMem[succ(Adr)]);

        SL[succ(succ(Adr))]:=Round(Brightness*SLMem[succ(succ(Adr))]);

       end

   end;

end;

 

Při konstrukci připojeného obrázku byl stín navíc poněkud rozmazán obrazovým filtrem typu dolní propust, čímž jsou simulovány nenulové rozměry světelného zdroje a dále započten úbytek intenzity světla se vzrůstající vzdáleností.

 

 

Zde najdete kompletní             zdrojový kód               a zde                spustitelný kód

 

Průhledné plochy

 

Dosud jsme předpokládali, že objekty, které sestrojujeme, jsou neprůhledné a neprůsvitné, tj. že dopadající světlo je odraženo popř. pohlceno a žádné neprochází. Pro řadu objektů však tento předpoklad nemusí vždy platit. Průhledným objektem přitom rozumíme objekt, kterým se světlo šíří podle zákonů geometrické optiky, tj. uvažujeme pouze odraz a lom světla, event úbytek intenzity v závislosti na vzdálenosti. Průsvitným objektem rozumíme objekt, kde je třeba brát v úvahu i vlnovou podstatu světla, projevující se především jeho ohybem. Při algoritmizaci nelze bohužel většinou postihnout celý popsaný fyzikální proces. Jednak pro celkovou složitost a jednak pro nedostupnost potřebných informací. U každého paprsku by totiž bylo třeba propočítat všechny odrazy a lomy a barvy interferujících paprsků míchat. Navíc by bylo třeba započítat další jevy - absorbci, difuzi, dispersi apod. To ovšem lze udělat pouze pro objekt, který lze popsat analyticky a pro jehož každý bod jsou tyto veličiny známy. Většina objektů (např. mikroskopovaný biologický preparát) těmto požadavkům ani přibližně nevyhovuje. Přesto je možné dosti uspokojivě vzhled takového objektu popsat tak, že budeme uvažovat vždy jen poměr intenzit odraženého a lomeného paprsku. Ostatní veličiny - změna směru vlivem lomu, difuzi, dispersi apod. - nebudeme uvažovat. Vpodstatě to znamená, že budeme prozatím uvažovat pouze nekonečně tenké průhledné plochy.

 

Za výše uvedených zjednodušujících předpokladů není programová realizace příliš složitá. Plochu tentokrát sestrojujeme pomocí průhledných segmentů. Jejich vrcholy je třeba promítnout do průmětny a pak je možno použít výše zmíněnou proceduru FillTriangle2D, je však třeba modifikovat řádky označené tam jako {sestrojení úsečky}. Procedura FillTriangle2D, jak již bylo uvedeno, vyplní celý trojúhelník konstantní barvou. V případě průhledného či průsvitného povrchu je však třeba barvu každého pixelu míchat minimálně ze dvou barev - barvy odraženého a prošlého světla. Předmětné řádky kódu

 

 For i:=h1 to h2 do

  begin

   Adr:=3*i;

   ScanRow[Adr]:=Blue;

   ScanRow[succ(Adr)]:=Green;

   ScanRow[succ(succ(Adr))]:=Red;

  end;

 

sestrojí ve fyzické rovině vodorovnou úsečku spojující fyzické pixely o souřadnicích ; , a to konstantní barvou, která je dána konstantními hodnotami barevných složek Red, Green, Blue (barva odraženého světla). Tuto barvu je však nyní potřeba smíchat s barvou světla prošlého, a to pixel po pixelu a složku po složce. V řešeném příkladu je prošlé, odražené i složené světlo deklarováno jako záznam tří barevných složek - record Red, Green,Blue:Byte;end. Barva odraženého světla je určena barvou texturz, barvu prošlého světla budeme modelovat pomocí pixelu, který je překreslován. Intenzita prošlého resp. odraženého světla je uložena v proměnné Int_P resp Int_O:

 

for i:=h1 to h2 do

begin

 Adr:=3*i;

 With Michane do

   begin

    Red:=Trunc((Int_P*Prosle[i,j].Red

                   +Int_O*Odrazene[i,j].Red)/(Int_P+Int_O));

    Green:=Trunc((Int_P*Prosle.Green[i,j]

                   +Int_O*Odrazene[i,j].Green)/(Int_P+Int_O));

    Blue:=Trunc((Int_P*Prosle.Blue[i,j]

                   +Int_O*Odrazene[i,j].Blue)/(Int_P+Int_O));

    ScanRow[Adr]:=Blue;

    ScanRow[succ(Adr)]:=Green;

    ScanRow[succ(succ(Adr))]:=Red;

  end;

 end;

 

Příklad 1.: Dutá průhledná zeměkoule. Zde je naprogramován povrch zeměkoule tak, jak by vypadal v případě, že by byl nahrazen nekonečně tenkou průhlednou kulovou plochou.

 

 

 

Zde najdete      kompletní zdrojový kód                       a zde                           spustitelný kód

 

Příklad 2.: Průhledná plocha zadaná rovnicí . V kpt. 8.2. jsme popisovali sestrojení plochy zadané rovnicí  a v kpt. 8.3. jsme ji sestrojili pomocí interpolace stínu (viz. kpt. 8.3. př. 9). Zde je plocha sestrojena pomocí průhledných segmentů dle výše uvedeného algoritmu.

 

Zde najdete      kompletní zdrojový kód                       a zde                           spustitelný kód