コンピュータジオメトリの基礎。簡単な3Dレンダリングを書く

こんにちは、私の名前はデビッドです。ここで私は自分自身で、自分の手描きのレンダリングでレンダリングされています。



画像


残念ながら、これ以上の品質の無料モデルは見つかりませんでしたが、それでも私をデジタルで捉えてくれた海外の彫刻家に感謝します!ご想像のとおり、CPUの作成について説明します-レンダリング。



考え



シェーダー言語の開発とGPUパワーの増加に伴い、ますます多くの人々がグラフィックプログラミングに興味を持っています。その人気が急速に高まっているレイマーチングなど、新しい方向性が現れました。



NVidiaからの新しいモンスターのリリースを見越して、CPUでのレンダリングの基本について自分の(チューブとオールドスクールの)記事を書くことにしました。これは、レンダリングを作成した私の個人的な経験を反映しており、コーディングプロセスで遭遇した概念とアルゴリズムを伝えようとします。このようなタスクを実行するためのプロセッサが不適切であるため、このソフトウェアのパフォーマンスは非常に低くなることを理解する必要があります。



言語の選択は最初はc ++または錆に落ちましたが、私はc#に落ち着きましたコードの記述が簡単で、最適化の機会が十分にあるためです。この記事の最終製品は、次のような画像を生成できるレンダリングになります。



画像


画像


ここで使用したすべてのモデルはパブリックドメインで配布されており、アーティストの作品を海賊行為したり尊重したりしないでください。



数学



数学的な基礎を理解せずにレンダリングをどこに書くかは言うまでもありません。このセクションでは、コードで使用した概念のみを取り上げます。知識がわからない人には、このセクションをスキップすることをお勧めしません。これらの基本を理解しないと、それ以上のプレゼンテーションを理解するのは困難です。また、計算ジオメトリを研究することを決定した人は、線形代数、ジオメトリ、および三角測量(角度、ベクトル、行列、ドット積)の基本的な知識を持っていることを期待しています。計算ジオメトリをより深く理解したい人には、E。Nikulinによる本「ComputerGeometry and ComputerGraphicsAlgorithms」をお勧めします。



ベクトルが回転します。回転行列



回転は、ベクトル空間の基本的な線形変換の1つです。変換されたベクトルの長さを保持するため、直交変換でもあります。2D空間での回転には2つのタイプがあります。



  • 原点に対する回転
  • ある点を中心に回転


ここでは、最初のタイプのみを検討します。2番目は最初の派生物であり、回転座標系の変更のみが異なります(座標系をさらに分析します)。



2次元空間でベクトルを回転させるための式を導き出しましょう。元のベクトルの座標を示しましょう- {x、y}角度fだけ回転した新しいベクトルの座標は、{x'y '}として示されます。



画像


これらのベクトルの長さは一般的であることがわかっているため、これらのベクトルをOX軸を中心とした長さと角度で表すために、コサインとサインの概念を使用できます



画像


合計式と余弦式を使用して、x 'y'値を展開できることに注意してください忘れてしまった人のために、私はこれらの公式を思い出させます:



画像


それらを通して回転したベクトルの座標を展開すると、次のようになります。



画像


ここで、係数l * cosaおよびl * sin aが元のベクトルの座標であることが簡単にわかりますx = l * cos a、y = l * sinaそれらをxyに置き換えましょう



画像


したがって、元のベクトルの座標とその回転角度で回転ベクトルを表現しました。マトリックスとして、この式は次のようになります。



画像


乗算して、結果が私たちが推定したものと同等であることを確認します。



3D空間で回転



二次元空間での回転を考慮し、そのための行列も導き出しました。ここで問題が発生します。3次元でこのような変換を取得するにはどうすればよいでしょうか。 2次元の場合、平面上でベクトルを回転させましたが、ここでは、これを実行できる平面の数が無限にあります。ただし、3次元空間でベクトルの任意の回転を表現できる回転には、基本的に3つのタイプがありますこれらは、XYXZYZ回転です。



XY回転。



この回転で、座標系のOZ軸を中心にベクトルを回転させます。ベクトルがヘリコプターのブレードであり、OZがそれらが保持するマストであると想像してくださいXYベクトルの回転は、マストに対するヘリコプターのブレードのように、OZ軸を中心に回転します。



画像


この回転と、そのノートのzベクトルの座標は変更されませんが、XおよびX座標の変化-これはと呼ばれている理由ですXYの回転。



画像


:これは、このような回転のために導出式に困難ではないZ座標残っ同じ、および- XYの2次元回転と同じ原理に従って変化します。



画像


マトリックスの形で同じ:



画像


以下のためにXZYZ回転、すべてが同じです。



画像
画像


投影



投影の概念は、それが使用されるコンテキストによって異なります。多くの人は、平面への投影や座標軸への投影などの概念について聞いたことがあるでしょう。



ここで使用する理解では、ベクトルへの投影もベクトルです。その座標は、ベクトルaからbにドロップされ垂線とベクトルbの交点です。



画像


このようなベクトルを定義するには、その長さ方向を知る必要がありますご存知のように、直角三角形の隣接する脚とハイポテヌスはコサイン比によって関連付けられているため、これを使用して投影ベクトルの長さを表します。



画像


定義による投影ベクトルの方向は、ベクトルbと一致します。これは、投影が次の式によって決定されることを意味します。



画像


ここでは、投影の方向を単位ベクトルとして取得し、それを投影の長さで乗算します。結果がまさに私たちが探しているものになることを理解するのは難しいことではありません。



それでは、ドット製品の観点からすべてを表現しましょう



画像


射影を見つけるための便利な式が得られます。



画像


調整システム。基地



多くの人は、標準のXYZ座標系での作業に慣れています。このシステムでは、任意の2つの軸が互いに垂直になり、座標軸は単位ベクトルとして表すことができます。



画像


実際、座標系は無限にあり、それぞれが基礎となっていますn次元空間の基礎は、この空間のすべてのベクトルが表される一連のベクトル{v1、v2……vn}です。この場合、基底からのベクトルを他のベクトルで表すことはできません。実際、各基底は個別の座標系であり、ベクトルには独自の一意の座標があります。



二次元空間の基礎が何であるかを見てみましょう。たとえば、2次元空間のベースの1つであるベクトルX {1、0}Y {0、1 }のよく知られたカルテシアン座標系を考えてみましょう



画像




平面上の任意のベクトルは、特定の係数を持つこの基底のベクトルの合計として、または線形の組み合わせとして表すことができますベクトルの座標を書き留めるときの操作を覚えておいてください。xを書き、次に座標を書き、次にyを書きますこれは、基本ベクトルの観点から拡張係数を実際に決定する方法です。



画像




次に、別の基準を取りましょう。



画像




2Dベクトルは、そのベクトルを介して表すこともできます。



画像




しかし、そのようなベクトルのセットは 2次元空間の基礎ではありません



画像




その中で、2つのベクトル{1,1}{2,2}が1つの直線上にあります。どのような組み合わせをとっても、共通の直線y = x上にあるベクトルのみを受け取ります。私たちの目的では、そのような欠陥のあるものは役に立ちませんが、違いを理解する価値があると思います。定義上、すべての基底は1つのプロパティによって統合されます。つまり、他の基底ベクトルと係数の合計として表すことができない基底ベクトルも、他の基底ベクトルの線形結合でもありません。これも基礎ではない3つのベクトルのセットの例です



画像




二次元平面の任意のベクターは、それを介して発現させることができるが、ベクトル{1,1} それ自体がベクターを介して発現させることができるので、それには、不必要である{1,0}および{0,1}として、{1,0} + {0,1 }



一般に、n次元空間の基底には、正確にn個のベクトルが含まれます。2eの場合、このnはそれぞれ2に等しくなります。3dに目を向け



ましょう。3次元ベースには、次の3つのベクトルが含まれます。



画像




2次元ベースの場合、1つの直線上にない2つのベクトルで十分だった場合、3次元空間では、次の場合にベクトルのセットがベースになります。



  • 1)2つのベクトルが1つの直線上にない
  • 2)3番目は他の2つによって形成された平面上にありません




今後、作業するベースは直交し(ベクトルはすべて垂直)、正規化されます(ベースベクトルの長さは1です)。私たちは単に他の人を必要としません。たとえば、標準ベース



画像




これらの基準を満たしています。



別の基盤への移行



これまで、ベクトルの分解を、係数を持つ基底ベクトルの合計として記述してきました。



画像


標準的な基準をもう一度考えてみましょう。その中のベクトル{1、3、6}は次のように書くことができます。



画像


ご覧のとおり、基底内のベクトルの展開係数は、この基底内の座標です。次の例を見てみましょう。



画像




この基準は、45度のXY回転を適用することによって標準から導き出されます。座標{0、1、1}の標準システムでベクトルa取ります



画像




新しい基底のベクトルを介して、次のように拡張できます。



画像




この量を計算すると、{0、1、1}(標準ベースのベクトルa)が得られます。新しい基準でのこの式に基づいて、ベクトルaは座標{0.7、0.7、1} -展開係数を持ちます。これは、別の角度から見るとより見やすくなります。



画像




しかし、これらの係数をどのように見つけますか?一般に、普遍的な方法は、線形方程式のかなり複雑なシステムの解決策です。ただし、前に述べたように、直交ベース正規化ベースのみを使用し、それらには非常に不正な方法があります。それは、基底ベクトルへの射影見つけることにあります。これを使用して、基底X {0.7、0.7、0} Y {-0.7、0.7、0} Z {0、0、1}のベクトルaの分解を見つけましょう



画像




まず、y 'の係数を見つけましょう最初のステップは、ベクトルaのベクトルy 'への投影を見つけることです(これを行う方法については上記で説明しました)。



画像




第二ステップ:私たちは、ベクトルの長さによって発見投影の長さを分割する「Y「投影ベクトルに収まるので、私たちはどのように多くのベクトルyは」見つける、」 -この番号は、の係数となり、Y '、また、Y -ベクトルの座標A新しい基礎で!以下のためにX「及びZ」は、同様の動作を繰り返します。



画像




これで、標準ベースから新しいベースへの移行の公式ができました。



画像




正規化された基底 のみを使用し、それらのベクトルの長さは1に等しいため、遷移式でベクトルの長さで除算する必要はありません。



画像




射影式を使用しx座標を展開します。



画像




正規化された基底の場合 の分母(x '、x')とベクトルx 'も1であり、破棄できることに注意してください。我々が得る:



画像




基底のx 座標はドット積(a、x ')y座標それぞれ(a、y')として表されz座標は(a、z ')であることがわかります。これで、新しい座標への遷移のマトリックスを作成できます



画像




オフセット座標系



上で検討したすべての座標系は、点{0,0,0}の原点を持っていましたさらに、原点がシフトしたシステムもあります。



画像




ベクトルをそのようなシステムに変換するには、最初に新しい座標の中心を基準にしてベクトルを表現する必要があります。これを行うには簡単です-ベクトルからこの中心を差し引きます。したがって、ベクトルをそのままにして、座標系自体を新しい中心に「移動」することができます。次に、すでにおなじみの遷移マトリックスを使用できます。



ジオメトリエンジンの作成。ワイヤーレンダリングを作成します。





さて、数学のセクションを通過し、記事を閉じなかった人は、もっと面白いことで洗脳できると思います!このセクションでは、3Dエンジンとレンダリングの基本を書き始めます。一般に、レンダリングはかなり複雑な手順であり、目に見えないエッジの切り取り、ラスター化、光の計算、さまざまな効果、マテリアル(場合によっては物理)の処理など、さまざまな操作が含まれます。将来的にはこれらすべてを部分的に分析しますが、今度はもっと簡単なことを行います。ワイヤーレンダリングを作成します。その本質は、頂点を結ぶ線の形でオブジェクトを描画することです。そのため、結果はワイヤのネットワークのように見えます。



画像




多角形のグラフィック



従来、コンピューターグラフィックスは、3Dオブジェクトデータのポリゴン表現を使用します。したがって、データはOBJ、3DS、FBXおよび他の多くで提示されます。コンピュータでは、このようなデータは、頂点のセットと面のセット(ポリゴン)の2つのセットの形式で保存されます。オブジェクトの各頂点は、空間内の位置(ベクトル)で表され、各面(ポリゴン)は、このオブジェクトの頂点のインデックスである3つの整数で表されます。最も単純なオブジェクト(キューブ、球など)は、このようなポリゴンで構成され、プリミティブと呼ばれます。



私たちのエンジンでは、プリミティブは3次元ジオメトリのメインオブジェクトになります-他のすべてのオブジェクトはそれを継承します。プリミティブのクラスについて説明しましょう。



    abstract class Primitive
    {
        public Vector3[] Vertices { get; protected set; }
        public int[] Indexes { get; protected set; }
    }


これまでのところ、すべてが単純です-プリミティブの頂点があり、ポリゴンを形成するためのインデックスがあります。これで、このクラスを使用してキューブを作成できます。



   public class Cube : Primitive
      {
        public Cube(Vector3 center, float sideLen)
        {
            var d = sideLen / 2;
            Vertices = new Vector3[]
                {
                    new Vector3(center.X - d , center.Y - d, center.Z - d) ,
                    new Vector3(center.X - d , center.Y - d, center.Z) ,
                    new Vector3(center.X - d , center.Y , center.Z - d) ,
                    new Vector3(center.X - d , center.Y , center.Z) ,
                    new Vector3(center.X + d , center.Y - d, center.Z - d) ,
                    new Vector3(center.X + d , center.Y - d, center.Z) ,
                    new Vector3(center.X + d , center.Y + d, center.Z - d) ,
                    new Vector3(center.X + d , center.Y + d, center.Z + d) ,
                };

            Indexes = new int[]
                {
                    1,2,4 ,
                    1,3,4 ,
                    1,2,6 ,
                    1,5,6 ,
                    5,6,8 ,
                    5,7,8 ,
                    8,4,3 ,
                    8,7,3 ,
                    4,2,8 ,
                    2,8,6 ,
                    3,1,7 ,
                    1,7,5
                };
        }
    }

int Main()
{
        var cube = new Cube(new Vector3(0, 0, 0), 2);
}


画像


座標系の実装



一連のポリゴンでオブジェクトを設定するだけでは不十分です。複雑なシーンを計画および作成するには、オブジェクトをさまざまな場所に配置し、回転させ、サイズを縮小または拡大する必要があります。これらの操作の便宜のために、いわゆるローカルおよびグローバル座標系が使用されます。シーン上の各オブジェクトには、独自の座標系(ローカル、および独自の中心点)があります。



画像


ローカル座標でのオブジェクトの表現により、オブジェクトを使用して任意の操作を簡単に実行できます。たとえば、オブジェクトをベクトルa移動するにこのベクトルで座標系の中心をシフトし、オブジェクトを回転させ、ローカル座標を回転させるだけで十分です。



オブジェクトを操作するときは、ローカル座標系の頂点を使用して操作を実行します。レンダリング中に、最初にシーン内のすべてのオブジェクトを単一の座標系(グローバル座標系)に変換します。コードに座標系を追加しましょう。これを行うには、Pivo​​tクラスのオブジェクト(ピボット、ピボットポイント)を作成します。これは、オブジェクトとその中心点のローカルベースを表します。ポイントをピボットによって提示される座標系に変換することは、2つのステップで行われます。



  • 1)新しい座標の中心を基準にした点の表現
  • 2)新しい基底のベクトルの拡張


逆に、オブジェクトのローカル頂点をグローバル座標で表すには、次のアクションを逆の順序で実行する必要があります。



  • 1)グローバルベースのベクトルの拡大
  • 2)グローバルセンターに対する表現


座標系を表すクラスを書いてみましょう。



    public class Pivot
    {
        // 
        public Vector3 Center { get; private set; }
        //   -   
        public Vector3 XAxis { get; private set; }
        public Vector3 YAxis { get; private set; }
        public Vector3 ZAxis { get; private set; }

        //    
        public Matrix3x3 LocalCoordsMatrix => new Matrix3x3
            (
                XAxis.X, YAxis.X, ZAxis.X,
                XAxis.Y, YAxis.Y, ZAxis.Y,
                XAxis.Z, YAxis.Z, ZAxis.Z
            );

        //    
        public Matrix3x3 GlobalCoordsMatrix => new Matrix3x3
            (
                XAxis.X , XAxis.Y , XAxis.Z,
                YAxis.X , YAxis.Y , YAxis.Z,
                ZAxis.X , ZAxis.Y , ZAxis.Z
            );

        public Vector3 ToLocalCoords(Vector3 global)
        {
            //          
            return LocalCoordsMatrix * (global - Center);
        }
        public Vector3 ToGlobalCoords(Vector3 local)
        {
            //    -            
            return (GlobalCoordsMatrix * local)  + Center;
        }

        public void Move(Vector3 v)
        {
            Center += v;
        }

        public void Rotate(float angle, Axis axis)
        {
            XAxis = XAxis.Rotate(angle, axis);
            YAxis = YAxis.Rotate(angle, axis);
            ZAxis = ZAxis.Rotate(angle, axis);
        }
    }


ここで、このクラスを使用して、回転、移動、および増加の関数をプリミティブに追加します。



    public abstract class Primitive
    {
        //  
        public Pivot Pivot { get; protected set; }
        // 
        public Vector3[] LocalVertices { get; protected set; }
        // 
        public Vector3[] GlobalVertices { get; protected set; }
        // 
        public int[] Indexes { get; protected set; }

        public void Move(Vector3 v)
        {
            Pivot.Move(v);

            for (int i = 0; i < LocalVertices.Length; i++)
                GlobalVertices[i] += v;
        }

        public void Rotate(float angle, Axis axis)
        {
            Pivot.Rotate(angle , axis);

            for (int i = 0; i < LocalVertices.Length; i++)
                GlobalVertices[i] = Pivot.ToGlobalCoords(LocalVertices[i]);
        }

        public void Scale(float k)
        {
            for (int i = 0; i < LocalVertices.Length; i++)
                LocalVertices[i] *= k;

            for (int i = 0; i < LocalVertices.Length; i++)
                GlobalVertices[i] = Pivot.ToGlobalCoords(LocalVertices[i]);
        }
    }


画像


ローカル座標を使用したオブジェクトの回転と移動



ポリゴンの描画。カメラ



シーンの主なオブジェクトはカメラになります-それを使用して、オブジェクトが画面に描画されます。カメラは、シーン内のすべてのオブジェクトと同様に、Pivo​​tクラスのオブジェクトの形式でローカル座標を持ちます。これにより、カメラを移動および回転します。



画像


画面にオブジェクトを表示するには、単純な透視投影法を使用します。この方法の基礎となる原則は、オブジェクトが私たちから離れるほど小さく見えるということです。おそらく多くの人が学校で観察者から一定の距離にある木の高さを測定する問題を解決したことがあります。



画像


木の上からの光線が、観測者から距離C1ある特定の投影面当たり、その上に点を描くと想像してください観察者はこの点を見て、そこから木の高さを決定したいと考えています。ご覧のとおり、木の高さと投影面上の点の高さは、類似した三角形の比率によって関連付けられています。次に、オブザーバーはこの比率を使用してポイントの高さを決定できます。



画像




それどころか、木の高さを知っていると、彼は投影面上の点の高さを見つけることができます。



画像




それでは、カメラに戻りましょう。原点から距離z 'のところにカメラのz軸に取り付けられた投影面がある想像してくださいこのような平面の式はz = z 'であり、1つの数値--z'で与えることができますさまざまなオブジェクトの頂点からの光線がこの平面に当たります。光線が飛行機に当たると、その上にポイントが残ります。このような点をつなぐことで、オブジェクトを描くことができます。



画像




この平面は画面を表します。画面上のオブジェクトの頂点の投影の座標を2段階で見つけます。



  • 1)頂点をカメラのローカル座標に変換します
  • 2)類似した三角形の比率から点の投影を見つけます


画像




投影は2次元ベクトルになり、そのx 'およびy'座標がコンピューター画面上の点の位置を定義します。



チャンバークラス1
public class Camera
{
    //  
    public Pivot Pivot { get; private set; }
    //   
    public float ScreenDist { get; private set; }

    public Camera(Vector3 center, float screenDist)
    {
        Pivot = new Pivot(center);
        ScreenDist = screenDist;
    }
    public void Move(Vector3 v)
    {
        Pivot.Move(v);
    }
    public void Rotate(float angle, Axis axis)
    {
        Pivot.Rotate(angle, axis);
    }
    public Vector2 ScreenProection(Vector3 v)
    {
        var local = Pivot.ToLocalCoords(v);
        //    
        var delta = ScreenDist / local.Z;
        var proection = new Vector2(local.X, local.Y) * delta;
        return proection;
    }
}




このコードにはいくつかのエラーがありますが、後で修正する方法について説明します。



見えないポリゴンを切り落とす



このようにポリゴンの3点を画面に投影すると、画面上のポリゴンの表示に対応する三角形の座標が得られます。しかし、このようにして、カメラは、投影が画面領域を超える頂点を含むすべての頂点を処理します。そのような頂点を描画しようとすると、エラーが発生する可能性が高くなります。カメラは、背後にあるポリゴンも処理します(ローカルカメラベースライン内のポイントのz座標はz '未満です)。このような「後頭部」ビジョンも必要ありません。



画像




開いたglで非表示の頂点をクリッピングするには、切り捨てピラミッド法が使用されます。それは2つの平面を設定することから成ります-近く(近くの平面)と遠く(遠い平面)。これらの2つの平面の間にあるものはすべて、さらに処理されます。私は1つのクリッピング平面を持つ簡略化されたバージョンを使用します-z 'その背後にあるすべての頂点は非表示になります。



画面の幅と高さの2つの新しいフィールドをカメラに追加しましょう。

次に、各投影ポイントが画面領域に当たっているかどうかを確認します。カメラの後ろのポイントも切り取りましょう。ポイントが後ろにあるか、その投影が画面に当たらない場合、メソッドはポイント{float.NaN、float.NaN}を返します



カメラコード2
public Vector2 ScreenProection(Vector3 v)
{
    var local = Pivot.ToLocalCoords(v);
    //   
    if (local.Z < ScreenDist)
    {
        return new Vector2(float.NaN, float.NaN);
    }
    //    
    var delta = ScreenDist / local.Z;
    var proection = new Vector2(local.X, local.Y) * delta;
    //     -  
    if (proection.X >= 0 && proection.X < ScreenWidth && proection.Y >= 0 && proection.Y < ScreenHeight)
    {
        return proection;
    }
    return new Vector2(float.NaN, float.NaN);
}




画面座標への変換



ここでポイントを明確にします。多くのグラフィックライブラリでは、描画が画面座標系で行われるという事実に関連しています。このような座標では、原点は画面の左上の点であり、右に移動するxが増加し、下に移動するとyが増加します。投影面では、ポイントは通常のデカルト座標で表され、描画する前に、これらの座標を画面座標に変換する必要があります。これを行うのは難しくありません。原点を左上隅シフトしてyを反転するだけです



画像




カメラコード3
public Vector2 ScreenProection(Vector3 v)
{
    var local = Pivot.ToLocalCoords(v);
    //   
    if (local.Z < ScreenDist)
    {
        return new Vector2(float.NaN, float.NaN);
    }
    //    
    var delta = ScreenDist / local.Z;
    var proection = new Vector2(local.X, local.Y) * delta;
    //        
    var screen = proection + new Vector2(ScreenWidth / 2, -ScreenHeight / 2);
    var screenCoords = new Vector2(screen.X, -screen.Y);
    //     -  
    if (screenCoords.X >= 0 && screenCoords.X < ScreenWidth && screenCoords.Y >= 0 && screenCoords.Y < ScreenHeight)
    {
        return screenCoords;
    }
    return new Vector2(float.NaN, float.NaN);
}




投影画像のサイズを調整する



前のコードを使用してオブジェクトを描画すると、次のようになります。



画像




何らかの理由で、すべてのオブジェクトは非常に小さく描画されます。理由を理解するために、投影法をどのように計算したかを思い出してください。x座標y座標にz '/ z比のデルタを掛けました。これは、画面上のオブジェクトのサイズが投影面z 'までの距離に依存することを意味します。ただし、z 'は必要なだけ小さく設定できます。したがって、現在のz '値に応じて投影サイズを調整する必要があります。これを行うには、カメラに別のフィールド(視野角)を追加しましょう



画像




画面の角度サイズと を一致させる必要があります。このようにして、角度が画面の幅に一致します。カメラが見ている最大角度は、画面の左端または右端です。その場合、カメラのz軸からの最大角度o / 2です。画面の右端に落ちた投影は、x =幅/ 2座標で、左側はx = -width / 2である必要があります。これを知って、投影ストレッチ係数を見つけるための式を導き出します。



画像




カメラコード4
public float ObserveRange { get; private set; }
public float Scale => ScreenWidth / (float)(2 * ScreenDist * Math.Tan(ObserveRange / 2));
public Vector2 ScreenProection(Vector3 v)
{
    var local = Pivot.ToLocalCoords(v);
    //   
    if (local.Z < ScreenDist)
    {
        return new Vector2(float.NaN, float.NaN);
    }
    //          
    var delta = ScreenDist / local.Z * Scale;
    var proection = new Vector2(local.X, local.Y) * delta;
    //        
    var screen = proection + new Vector2(ScreenWidth / 2, -ScreenHeight / 2);
    var screenCoords = new Vector2(screen.X, -screen.Y);
    //     -  
    if (screenCoords.X >= 0 && screenCoords.X < ScreenWidth && screenCoords.Y >= 0 && screenCoords.Y < ScreenHeight)
    {
        return screenCoords;
    }
    return new Vector2(float.NaN, float.NaN);
}




テストに使用した簡単なレンダリングコードは次のとおりです。



オブジェクト描画コード
public DrawObject(Primitive primitive , Camera camera)
{
    for (int i = 0; i < primitive.Indexes.Length; i+=3)
    {
        var color = randomColor();
        //   
        var i1 = primitive.Indexes[i];
        var i2 = primitive.Indexes[i+ 1];
        var i3 = primitive.Indexes[i+ 2];
        //  
        var v1 = primitive.GlobalVertices[i1];
        var v2 = primitive.GlobalVertices[i2];
        var v3 = primitive.GlobalVertices[i3];
        //  
        DrawPolygon(v1,v2,v3 , camera , color);
    }
}

public void DrawPolygon(Vector3 v1, Vector3 v2, Vector3 v3, Camera camera , color)
{
    // 
    var p1 = camera.ScreenProection(v1);
    var p2 = camera.ScreenProection(v2);
    var p3 = camera.ScreenProection(v3);
    // 
    DrawLine(p1, p2 , color);
    DrawLine(p2, p3 , color);
    DrawLine(p3, p2 , color);
}




シーンとキューブのレンダリングを確認してみましょう。



画像




そして、はい、すべてがうまく機能します。カラフルなキューブを大げさに感じない人のために、OBJ形式のモデルをプリミティブオブジェクトに解析し、背景を黒で塗りつぶし、いくつかのモデルをレンダリングする関数を作成しました。



レンダリングの結果


画像



画像





ポリゴンのラスター化。私たちは美しさをもたらします。





前のセクションでは、ワイヤーフレームレンダリングを作成しました。次に、その近代化について説明します。ポリゴンのラスター化を実装します。ポリゴンを



単にラスター化するということは、その上にペイントすることを意味します。既製の三角形のラスター化関数がすでにあるのに、なぜ自転車を書くのかと思われるでしょう。デフォルトのツールですべてを描画すると、次のようになります。



画像




現代美術では、正面のポリゴンの後ろにポリゴンが描かれています。つまり、お粥です。また、この方法でオブジェクトをどのようにテクスチャリングしますか?はい、ありません。したがって、独自のimbaラスタライザーを作成する必要があります。これにより目に見えないポイント、テクスチャ、さらにはシェーダーさえも切り取ることができます。しかし、これを行うには、一般的に三角形をペイントする方法を理解する価値があります。



線画のためのブレセンハムのアルゴリズム。



行から始めましょう。ブレセンハムのアルゴリズムを知らない人がいたら、これがコンピューターグラフィックスで直線を描くための主要なアルゴリズムです。彼またはその変更は、文字通りどこでも使用されます:線、セグメント、円などを描画します。より詳細な説明に興味がある人-wikiを読んでください。Bresenhamのアルゴリズム



{x1、y1}{x2、y2}を結ぶ線セグメントがあります。それらの間にセグメントを描画するには、その上にあるすべてのピクセルをペイントする必要があります。セグメントの2つのポイントについて、それらが存在するピクセルのx座標を見つけることができます座標x1とx2のすべての部分取得する必要があります。セグメント上のピクセルをペイントするために、x1からx2までのサイクルを開始し、各反復で計算しますy-線上にあるピクセルの座標。コードは次のとおりです。



void Brezenkhem(Vector2 p1 , Vector2 p2)
{
    int x1 = Floor(p1.X);
    int x2 = Floor(p2.X);
    if (x1 > x2) {Swap(x1, x2); Swap(p1 , p2);}
    float d = (p2.Y - p1.Y) / (x2 - x1);
    float y = p1.Y;
    for (int i = x1; i <= x2; i++)
    {
        int pixelY = Floor(y);
        FillPixel(i , pixelY);
        y += d;
    }
}


画像


ウィキからの画像



三角形をラスタライズします。充填アルゴリズム



線の描き方は知っていますが、三角形の場合は少し難しくなります(それほど難しくありません)。三角形を描くタスクは、線を描くいくつかのタスクに削減されます。まず、レッツ・スプリット2つの部分に三角形、以前の昇順でポイントをソートしたX



画像




注意-これで、上の境界線が明確に表現された2つの部分ができました残っているのは、間にあるすべてのピクセルを埋めることだけです!これは、x1からx2およびx3からx2の2サイクルで実行できます



void Triangle(Vector2 v1 , Vector2 v2 , Vector2 v3)
{
    // BubbleSort    x
    if (v1.X > v2.X) { Swap(v1, v2); }
    if (v2.X > v3.X) { Swap(v2, v3); }
    if (v1.X > v2.X) { Swap(v1, v2); }

    //    y    x
    //   0:  x1 == x2     - 
    var steps12 = max(v2.X - v1.X , 1);
    var steps13 = max(v3.X - v1.X , 1);
    var upDelta = (v2.Y - v1.Y) / steps12;
    var downDelta = (v3.Y - v1.Y) / steps13;

    //     
    if (upDelta < downDelta) Swap(upDelta , downDelta);

    //     y1
    var up = v1.Y;
    var down = v1.Y;

    for (int i = (int)v1.X; i <= (int)v2.X; i++)
    {
        for (int g = (int)down; g <= (int)up; g++)
        {
            FillPixel(i , g);
        }
        up += upDelta;
        down += downDelta;
    }

    //       
    var steps32 = max(v2.X - v3.X , 1);
    var steps31 = max(v1.X - v3.X , 1);
    upDelta = (v2.Y - v3.Y) / steps32;
    downDelta = (v1.Y - v3.Y) / steps31;

    if (upDelta < downDelta) Swap(upDelta, downDelta);

    up = v3.Y;
    down = v3.Y;

    for (int i = (int)v3.X; i >=(int)v2.X; i--)
    {
        for (int g = (int)down; g <= (int)up; g++)
        {
            FillPixel(i, g);
        }
        up += upDelta;
        down += downDelta;
    }
}


間違いなく、このコードはリファクタリングでき、ループを複製しないようにすることができます。



void Triangle(Vector2 v1 , Vector2 v2 , Vector2 v3)
{
    if (v1.X > v2.X) { Swap(v1, v2); }
    if (v2.X > v3.X) { Swap(v2, v3); }
    if (v1.X > v2.X) { Swap(v1, v2); }

    var steps12 = max(v2.X - v1.X , 1);
    var steps13 = max(v3.X - v1.X , 1);
    var steps32 = max(v2.X - v3.X , 1);
    var steps31 = max(v1.X - v3.X , 1);

    var upDelta = (v2.Y - v1.Y) / steps12;
    var downDelta = (v3.Y - v1.Y) / steps13;
    if (upDelta < downDelta) Swap(upDelta , downDelta);

    TrianglePart(v1.X , v2.X , v1.Y , upDelta , downDelta);

    upDelta = (v2.Y - v3.Y) / steps32;
    downDelta = (v1.Y - v3.Y) / steps31;
    if (upDelta < downDelta) Swap(upDelta, downDelta);

    TrianglePart(v3.X, v2.X, v3.Y, upDelta, downDelta);
}

void TrianglePart(float x1 , float x2 , float y1  , float upDelta , float downDelta)
{
    float up = y1, down = y1;
    for (int i = (int)x1; i <= (int)x2; i++)
    {
        for (int g = (int)down; g <= (int)up; g++)
        {
            FillPixel(i , g);
        }
        up += upDelta; down += downDelta;
    }
}


見えないポイントを切り取る。



まず、あなたがどのように見えるかを考えてください。今、あなたの前にスクリーンがあり、その後ろにあるものはあなたの目から隠されています。レンダリングでは、同様のメカニズムが機能します。あるポリゴンが別のポリゴンとオーバーラップする場合、レンダリングはオーバーラップしたポリゴンの上にそれを描画します。それどころか、ポリゴンの閉じた部分は描画されません。



画像




ポイントが表示されているかどうかを理解するために、レンダリングでzbufferメカニズム(深度バッファー)が使用されますzbufferは、width * heightの2次元配列(1次元に圧縮可能)と考えることができます。画面上の各ピクセルについて、z値(このポイントが投影された元のポリゴン上の座標)が格納さます。したがって、ポイントがオブザーバーに近いほど、そのz座標は小さくなります。最終的に、複数のポイントの投影が一致する場合は、最小のz座標でポイントをラスター化する必要があります



画像




ここで疑問が生じます-元のポリゴン上の点z座標を見つける方法は?これはいくつかの方法で行うことができます。たとえば、カメラの原点から光線を撮影し、投影面{x、y、z '}上の点を通過して、ポリゴンとの交点を見つけることができます。ただし、交差点を探すのは非常にコストがかかるため、別の方法を使用します。三角形を描くために、その投影の座標補間しました。これに加えて、元のポリゴンの座標も補間します。非表示のポイントを切り取るために、ラスター化メソッドで現在のフレームのzbuffer状態使用します。



私のzbufferは次のようになりますするVector3は、[] -座標を-それだけでなく、Zを含有するだけでなく、各スクリーンピクセル用のポリゴン点(フラグメント)の値を補間します。これはメモリを節約するために行われます。将来的には、シェーダーを作成するためにこれらの値が必要になるためです。それまでの間、表示される頂点(フラグメント)を判別するための次のコードがあります



コード
public void ComputePoly(Vector3 v1, Vector3 v2, Vector3 v3 , Vector3[] zbuffer)
{
    //  
    var v1p = Camera.ScreenProection(v1);
    var v2p = Camera.ScreenProection(v2);
    var v3p = Camera.ScreenProection(v3);

    //   x - 
    //,     -    
    if (v1p.X > v2p.X) { Swap(v1p, v2p); Swap(v1p, v2p); }
    if (v2p.X > v3p.X) { Swap(v2p, v3p); Swap(v2p, v3p); }
    if (v1p.X > v2p.X) { Swap(v1p, v2p); Swap(v1p, v2p); }

    //       
    int x12 = Math.Max((int)v2p.X - (int)v1p.X, 1);
    int x13 = Math.Max((int)v3p.X - (int)v1p.X, 1);

    //       
    float dy12 = (v2p.Y - v1p.Y) / x12; var dr12 = (v2 - v1) / x12;
    float dy13 = (v3p.Y - v1p.Y) / x13; var dr13 = (v3 - v1) / x13;

    Vector3 deltaUp, deltaDown; float deltaUpY, deltaDownY;
    if (dy12 > dy13) { deltaUp = dr12; deltaDown = dr13; deltaUpY = dy12; deltaDownY = dy13;}
    else { deltaUp = dr13; deltaDown = dr12; deltaUpY = dy13; deltaDownY = dy12;}

    TrianglePart(v1 , deltaUp , deltaDown , x12 , 1 , v1p , deltaUpY , deltaDownY , zbuffer);
    //    -   
}
public void ComputePolyPart(Vector3 start, Vector3 deltaUp, Vector3 deltaDown,
    int xSteps, int xDir, Vector2 pixelStart, float deltaUpPixel, float deltaDownPixel , Vector3[] zbuffer)
{
    int pixelStartX = (int)pixelStart.X;
    Vector3 up = start - deltaUp, down = start - deltaDown;
    float pixelUp = pixelStart.Y - deltaUpPixel, pixelDown = pixelStart.Y - deltaDownPixel;
    for (int i = 0; i <= xSteps; i++)
    {
        up += deltaUp; pixelUp += deltaUpPixel;
        down += deltaDown; pixelDown += deltaDownPixel;
        int steps = ((int)pixelUp - (int)pixelDown);
        var delta = steps == 0 ? Vector3.Zero : (up - down) / steps;
        Vector3 position = down - delta;
        for (int g = 0; g <= steps; g++)
        {
            position += delta;
            var proection = new Point(pixelStartX + i * xDir, (int)pixelDown + g);
            int index = proection.Y * Width + proection.X;
            //  
            if (zbuffer[index].Z == 0 || zbuffer[index].Z > position.Z)
            {
                zbuffer[index] = position;
            }
        }
    }
}




画像


ラスタライザーステップのアニメーション(zbufferで深度を書き換えると、ピクセルは赤で強調表示されます):



便宜上、すべてのコードを別のラスターライザーモジュールに移動しました。



ラスタライザークラス
    public class Rasterizer
    {
        public Vertex[] ZBuffer;
        public int[] VisibleIndexes;
        public int VisibleCount;
        public int Width;
        public int Height;
        public Camera Camera;

        public Rasterizer(Camera camera)
        {
            Shaders = shaders;
            Width = camera.ScreenWidth;
            Height = camera.ScreenHeight;
            Camera = camera;

        }
        public Bitmap Rasterize(IEnumerable<Primitive> primitives)
        {
            var buffer = new Bitmap(Width , Height);
            ComputeVisibleVertices(primitives);
            for (int i = 0; i < VisibleCount; i++)
            {
                var vec = ZBuffer[index];
                var proec = Camera.ScreenProection(vec);
                buffer.SetPixel(proec.X , proec.Y);
            }
            return buffer.Bitmap;
        }
        public void ComputeVisibleVertices(IEnumerable<Primitive> primitives)
        {
            VisibleCount = 0;
            VisibleIndexes = new int[Width * Height];
            ZBuffer = new Vertex[Width * Height];
            foreach (var prim in primitives)
            {
                foreach (var poly in prim.GetPolys())
                {
                    MakeLocal(poly);
                    ComputePoly(poly.Item1, poly.Item2, poly.Item3);
                }
            }
        }
        public void MakeLocal(Poly poly)
        {
            poly.Item1.Position = Camera.Pivot.ToLocalCoords(poly.Item1.Position);
            poly.Item2.Position = Camera.Pivot.ToLocalCoords(poly.Item2.Position);
            poly.Item3.Position = Camera.Pivot.ToLocalCoords(poly.Item3.Position);

        }
    }




それでは、レンダリング作業を確認しましょう。このために、私は有名なRPG「WOW」からのSylvanasのモデルを使用します。



画像




あまり明確ではありませんよね?これは、ここにテクスチャや照明がないためです。ただし、すぐに修正します。



テクスチャ!正常!点灯!モーター!



なぜすべてを1つのセクションにまとめたのですか?そして、本質的に、法線のテクスチャリングと計算は完全に同一であり、すぐにこれを理解するでしょう。



まず、1つのポリゴンのテクスチャリングタスクを見てみましょう。ここで、ポリゴンの頂点の通常の座標に加えて、そのテクスチャ座標も保存します。頂点のテクスチャ座標は2Dベクトルとして表され、テクスチャ画像のピクセルを指します。私はこれを示すためにインターネット上で良い写真を見つけました:



画像


テクスチャ座標 のテクスチャの開始(左下のピクセル)は{0、0}であり、終了(右上のピクセル)は{1、1}であることに注意してください。テクスチャ座標系と、テクスチャ座標が1の場合に画像の境界を超える可能性を考慮に入れてください。



頂点データをすぐに表すクラスを作成しましょう。



  public class Vertex
    {
        public Vector3 Position { get; set; }
        public Color Color { get; set; }
        public Vector2 TextureCoord { get; set; }
        public Vector3 Normal { get; set; }

        public Vertex(Vector3 pos , Color color , Vector2 texCoord , Vector3 normal)
        {
            Position = pos;
            Color = color;
            TextureCoord = texCoord;
            Normal = normal;
        }
    }


法線が必要な理由については後で説明します。今のところ、頂点が法線を持つことができることがわかります。ここで、ポリゴンをテクスチャ化するには、テクスチャから特定のピクセルにカラー値をマッピングする必要があります。頂点をどのように補間したか覚えていますか?ここでも同じことをしてください!ラスター化コードを再度書き直すことはしませんが、レンダリングにテクスチャリングを自分で実装することをお勧めします。その結果、モデルにテクスチャが正しく表示されます。これが私が得たものです:



テクスチャードモデル
画像




モデルのテクスチャ座標に関するすべての情報は、OBJファイルにあります。これを使用するには、次の形式を学習します:OBJ形式。



点灯





テクスチャを使用すると、すべてがはるかに楽しくなりますが、実際の楽しみは、シーンに照明を実装することです。「安い」照明をシミュレートするために、Phongモデルを使用ます。



フォンモデル



一般に、この方法は、背景(周囲)、散乱(拡散)、ミラー(反射)の3つの照明コンポーネントの存在をシミュレートします。これらの3つのコンポーネントの合計は、最終的に光の物理的な動作をシミュレートします。



画像


Phongモデル



Phong照明を計算するには、表面法線が必要です。このために、Vertexクラスにそれらを追加しました。これらの法線の値はどこにありますか?いいえ、何も計算する必要はありません。事実、寛大な3D編集者は、多くの場合、それらを自分検討し、OBJ形式のコンテキストでデータとともにモデルを提供します。モデルファイルを解析すると、各ポリゴンの3つの頂点の通常の値が得られます。



画像


wikiからの画像



ポリゴンの各ポイントで法線を計算するには、これらの値を補間する必要があります。これを行う方法はすでにわかっています。それでは、フォン照明を計算するためのすべてのコンポーネントを見てみましょう。



背景光(アンビエント)



最初に、一定の背景照明を設定します。テクスチャのないオブジェクトの場合、テクスチャのあるオブジェクトの任意の色を選択できます基本的なシェーディング(baseShading)の比率で各RGBコンポーネントを分割します



拡散光



光がポリゴンの表面に当たると、均一に散乱します。特定のピクセルの拡散を計算するために、光が表面に当たる角度が考慮されますこの角度を計算するには、入射光線と法線ドット積を適用できます(もちろん、ベクトルはその前に正規化する必要があります)。この角度は、光の強さの係数で乗算されます。ドット積が負の場合、ベクトル間の角度が90度より大きいことを意味します。この場合、ライトニングではなく、シェーディングの計算を開始します。この点を回避する価値があります。max関数を使用して実行できます



コード
public interface IShader
    {
        void ComputeShader(Vertex vertex, Camera camera);
    }

    public struct Light
    {
        public Vector3 Pos;
        public float Intensivity;
    }

public class PhongModelShader : IShader
    {
        public static float DiffuseCoef = 0.1f;
        public Light[] Lights { get; set; }

        public PhongModelShader(params Light[] lights)
        {
            Lights = lights;
        }
        public void ComputeShader(Vertex vertex, Camera camera)
        {
            if (vertex.Normal.X == 0 && vertex.Normal.Y == 0 && vertex.Normal.Z == 0)
            {
                return;
            }
            var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);
            foreach (var light in Lights)
            {
                var ldir = Vector3.Normalize(light.Pos - gPos);
                var diffuseVal = Math.Max(VectorMath.Cross(ldir, vertex.Normal), 0) * light.Intensivity;
                vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R * diffuseVal * DiffuseCoef),
                    (int)Math.Min(255, vertex.Color.G * diffuseVal * DiffuseCoef,
                    (int)Math.Min(255, vertex.Color.B * diffuseVal * DiffuseCoef));
            }
        }
    }




拡散した光を当てて、闇を払いのけましょう。



画像


ミラーライト(反射)



ミラーコンポーネントを計算するには、オブジェクトを見るポイントを考慮する必要があります次に、観測者からの光線表面から反射された光線のドット積に光強度係数を掛けたものを取得します。



画像


観察者から表面への光線を見つけるのは簡単です-それは単にローカル座標で処理された頂点の位置になります反射光線を見つけるために、私は次の方法を使用しました。入射光線は2つのベクトルに分解できます。法線への投影と、入射光線からこの投影を差し引くことによって見つけることができる2番目のベクトルです。反射光線を見つけるには、法線への投影から2番目のベクトルの値を差し引く必要があります。



コード
    public class PhongModelShader : IShader
    {
        public static float DiffuseCoef = 0.1f;
        public static float ReflectCoef = 0.2f;
        public Light[] Lights { get; set; }

        public PhongModelShader(params Light[] lights)
        {
            Lights = lights;
        }
        public void ComputeShader(Vertex vertex, Camera camera)
        {
            if (vertex.Normal.X == 0 && vertex.Normal.Y == 0 && vertex.Normal.Z == 0)
            {
                return;
            }
            var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);
            foreach (var light in Lights)
            {
                var ldir = Vector3.Normalize(light.Pos - gPos);
                //         
                var proection = VectorMath.Proection(ldir, -vertex.Normal);
                var d = ldir - proection;
                var reflect = proection - d;
                var diffuseVal = Math.Max(VectorMath.Cross(ldir, -vertex.Normal), 0) * light.Intensivity;
                //  
                var eye = Vector3.Normalize(-vertex.Position);
                var reflectVal = Math.Max(VectorMath.Cross(reflect, eye), 0) * light.Intensivity;
                var total = diffuseVal * DiffuseCoef + reflectVal * ReflectCoef;
                vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R * total),
                    (int)Math.Min(255, vertex.Color.G * total),
                    (int)Math.Min(255, vertex.Color.B * total));
            }
        }
    }




これで、画像は次のようになります。



画像






私のプレゼンテーションの終点は、レンダリング用のシャドウの実装です。私の頭蓋骨に端を発した最初の行き止まりのアイデアは、各ポイントとライトの間にポリゴンがあるかどうかをチェックすることです。そうである場合は、ピクセルを照らす必要はありません。 Sylvanasのモデルには22万を超えるポリゴンが含まれています。もしそうなら、これらすべてのポリゴンとの交差をチェックするために各ポイントについて、交差メソッドを最大220,000 * 1920 * 1080 * 219999で呼び出す必要があります! 10分で私のコンピューターはすべての計算の10番目の部分(220,000のうち2600ポリゴン)をマスターすることができました。その後、シフトがあり、新しい方法を探しました。



インターネットで、同じ計算を実行する非常にシンプルで美しい方法に出くわしました何千倍も速いこれはシャドウマッピング(シャドウマップの作成)と呼ばれます。オブザーバーに表示されるポイントをどのように決定したかを思い出してください。zbufferを使用しましたシャドウマッピングも同じです!最初のパスでは、カメラは明るい位置にあり、オブジェクトを見ています。これにより、光源の深度マップが生成されますデプスマップはおなじみのzbufferです。2番目のパスでは、このマップを使用して、どの頂点を照らすかを決定します。次に、適切なコードのルールを破り、チートパスを実行します。新しいラスタライザオブジェクトをシェーダーに渡すだけで、それを使用して深度マップを作成できます。



コード
public class ShadowMappingShader : IShader
{
    public Enviroment Enviroment { get; set; }
    public Rasterizer Rasterizer { get; set; }
    public Camera Camera => Rasterizer.Camera;
    public Pivot Pivot => Camera.Pivot;
    public Vertex[] ZBuffer => Rasterizer.ZBuffer;
    public float LightIntensivity { get; set; }

    public ShadowMappingShader(Enviroment enviroment, Rasterizer rasterizer, float lightIntensivity)
    {
        Enviroment = enviroment;
        LightIntensivity = lightIntensivity;
        Rasterizer = rasterizer;
        //     ,      
        //  /         
        Camera.OnRotate += () => UpdateDepthMap(Enviroment.Primitives);
        Camera.OnMove += () => UpdateDepthMap(Enviroment.Primitives);
        Enviroment.OnChange += () => UpdateDepthMap(Enviroment.Primitives);
        UpdateVisible(Enviroment.Primitives);
    }
    public void ComputeShader(Vertex vertex, Camera camera)
    {
        //   
        var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);
        //  
        var lghDir = Pivot.Center - gPos;
        var distance = lghDir.Length();
        var local = Pivot.ToLocalCoords(gPos);
        var proectToLight = Camera.ScreenProection(local).ToPoint();
        if (proectToLight.X >= 0 && proectToLight.X < Camera.ScreenWidth && proectToLight.Y >= 0
            && proectToLight.Y < Camera.ScreenHeight)
        {
            int index = proectToLight.Y * Camera.ScreenWidth + proectToLight.X;
            if (ZBuffer[index] == null || ZBuffer[index].Position.Z >= local.Z)
            {
                vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R + LightIntensivity / distance),
                    (int)Math.Min(255, vertex.Color.G + LightIntensivity / distance),
                    (int)Math.Min(255, vertex.Color.B + LightIntensivity / distance));
            }
        }
        else
        {
            vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R + (LightIntensivity / distance) / 15),
                    (int)Math.Min(255, vertex.Color.G + (LightIntensivity / distance) / 15),
                    (int)Math.Min(255, vertex.Color.B + (LightIntensivity / distance) / 15));
        }
    }
    public void UpdateDepthMap(IEnumerable<Primitive> primitives)
    {
        Rasterizer.ComputeVisibleVertices(primitives);
    }
}




静的なシーンの場合、深度マップの作成を1回呼び出してから、すべてのフレームで使用するだけで十分です。テストとして、私は銃のより少ない多角形のモデルを使用しています。これは出力画像です:



画像




あなたの多くはおそらくこのシェーダーのアーティファクト(光によって処理されていない黒い点)に気づいたでしょう。繰り返しになりますが、遍在するネットワークに目を向けると、この効果の説明が「シャドウアクネ」という厄介な名前で見つかりました(複雑な外観の人は許してください)。このような「ギャップ」の本質は、深度マップの限られた解像度を使用してシャドウを定義することです。これは、レンダリング時に複数の頂点が深度マップから1つの値を受け取ることを意味します。このアーティファクトの影響を最も受けやすいのは、光が穏やかな角度で当たる表面です。ライトのレンダリング解像度を上げることで効果を修正できますが、よりエレガントな方法があります。追加することで構成されています光ビームと表面の間の角度に応じた深さの特定のシフトこれは、ドット製品を使用して実行できます。



改善された影
public class ShadowMappingShader : IShader
{
    public Enviroment Enviroment { get; set; }
    public Rasterizer Rasterizer { get; set; }
    public Camera Camera => Rasterizer.Camera;
    public Pivot Pivot => Camera.Pivot;
    public Vertex[] ZBuffer => Rasterizer.ZBuffer;
    public float LightIntensivity { get; set; }

    public ShadowMappingShader(Enviroment enviroment, Rasterizer rasterizer, float lightIntensivity)
    {
        Enviroment = enviroment;
        LightIntensivity = lightIntensivity;
        Rasterizer = rasterizer;
        //     ,      
        //  /         
        Camera.OnRotate += () => UpdateDepthMap(Enviroment.Primitives);
        Camera.OnMove += () => UpdateDepthMap(Enviroment.Primitives);
        Enviroment.OnChange += () => UpdateDepthMap(Enviroment.Primitives);
        UpdateVisible(Enviroment.Primitives);
    }
    public void ComputeShader(Vertex vertex, Camera camera)
    {
        //   
        var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);
        //  
        var lghDir = Pivot.Center - gPos;
        var distance = lghDir.Length();
        var local = Pivot.ToLocalCoords(gPos);
        var proectToLight = Camera.ScreenProection(local).ToPoint();
        if (proectToLight.X >= 0 && proectToLight.X < Camera.ScreenWidth && proectToLight.Y >= 0
            && proectToLight.Y < Camera.ScreenHeight)
        {
            int index = proectToLight.Y * Camera.ScreenWidth + proectToLight.X;
            var n = Vector3.Normalize(vertex.Normal);
            var ld = Vector3.Normalize(lghDir);
            //  
            float bias = (float)Math.Max(10 * (1.0 - VectorMath.Cross(n, ld)), 0.05);
            if (ZBuffer[index] == null || ZBuffer[index].Position.Z + bias >= local.Z)
            {
                vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R + LightIntensivity / distance),
                    (int)Math.Min(255, vertex.Color.G + LightIntensivity / distance),
                    (int)Math.Min(255, vertex.Color.B + LightIntensivity / distance));
            }
        }
        else
        {
            vertex.Color = Color.FromArgb(vertex.Color.A,
                    (int)Math.Min(255, vertex.Color.R + (LightIntensivity / distance) / 15),
                    (int)Math.Min(255, vertex.Color.G + (LightIntensivity / distance) / 15),
                    (int)Math.Min(255, vertex.Color.B + (LightIntensivity / distance) / 15));
        }
    }
    public void UpdateDepthMap(IEnumerable<Primitive> primitives)
    {
        Rasterizer.ComputeVisibleVertices(primitives);
    }
}


画像




ボーナス

普通の人と遊ぶ



, , 3 . , .



image






:



            float angle = (float)Math.PI / 90;
            var shader = (preparer.Shaders[0] as PhongModelShader);
            for (int i = 0; i < 180; i+=2)
            {
                shader.Lights[0] = = new Light()
                    {
                        Pos = shader.Lights[0].Pos.Rotate(angle , Axis.X) ,
                        Intensivity = shader.Lights[0].Intensivity
                    };
                Draw();
            }


image



:



  • : 220 .

  • : 1920x1080.

  • : Phong model shader

  • : cpu — core i7 4790, 8 gb ram



FPS 1-2 /. realtime. , , .. cpu.



結論



私は自分自身を3Dグラフィックスの初心者だと思っています。プレゼンテーションの過程で犯した間違いを排除するものではありません。私が頼りにしているのは、創造の過程で得られた実際的な結果だけです。コメントにすべての修正と最適化(ある場合)を残すことができます。喜んで読みます。プロジェクトリポジトリへのリンク



All Articles