用C# Bitmap作爲畫布寫個3D軟渲染器

文章目錄


用C# Bitmap作爲畫布寫個3D軟渲染器

目的爲了瞭解,驗證圖形功能,加深基礎知識。
寫這個光柵渲染器過程,真的發現自己的數學知識不夠。
會有挺多卡點。
不知道誰可以介紹一些超級基礎數學知識的書本或是教程,或是學習方式。
關於:線性代數,幾何變換,仿射,微積分,的就好,其實我自己也搜索過很多資料。
就是沒找到好的資源,可能自己的基礎太差,沒法理解真正的幾何意義。

Recoards 記錄

圖元光柵

Start 開始
今天寫的柵格器,運行效果
在這裏插入圖片描述
發現對底層還是很多不瞭解的,要先暫停,看一下理論知識,總結差不多,再繼續寫

一邊學習OpenGL,一邊總結寫到軟渲染器中。
但是性能不用考慮,因爲我只渲染一幀。

法線渲染一幀都挺卡的(-_-!),特別是片段生成多的時候,更加卡。。。

Bitmap.SetPixel優化成LockBits/UnlockBits指針操作

2019.07.16 優化了Bitmap的像素設置方式。
才發現底層有更好的接口來畫畫布。
下面是設置了一個Timer.Interval = 15ms、10ms、1ms都試過。FPS都不變。
然後我將,Draw的代碼屏蔽掉,結果FPS還是一樣。
估計的Timer內部的Tick事件限制頻率了。
在這裏插入圖片描述

Blend

2019.07.17 添加了Blend
片段多了,性能還是上不去。-_-!
在這裏插入圖片描述

上面的GIF是舊的,因爲之前沒發現BitmapData的顏色指針的數據中,是BGRA而不是RGBA。
所以混合效果有些不太一樣,這個問題就修復。
在這裏插入圖片描述

Projection 投影

2019.07.17添加了透視相機、混合優化
投影效果:
渲染管線中的將幾何3D點集統一經過一系列的變換流程,最終生成2D窗口座標。
以下是變換順序:

  • Object Space Coordinates - Application to vertex data // 這時是沒有任何變換的局部模型座標系,
  • Clip Space Coordinates - Geometry // 有多個階段
    • VertexShader // 這一般由VertexShader負責逐頂點的變換,這裏的變換一般是先傳入來的Object Space Coordinates,然後根據用戶的可編程變換到對應的空間,這兒我們就變換到Clip Space Coordinates,所以,在這個階段我們需要MVP(Model & View & Project)矩陣,需要在Application 在對shader的uniform變量設置
      • MVP = MatrixprojectMatrixviewMatrixmodel/worldMatrix_{project} \cdot Matrix_{view} \cdot Matrix_{model/world}組合成的
      • Clip Space Pos = MVPObjectSpacePosMVP \cdot Object Space Pos
  • NDC, Window Coordinates - VertexShader Post-Processing // 是VertexShader後處理階段,一般就是Primitive Assembly,會對圖元生成(根據你指定的是Point,還是Line,Triange等),對其剪切(我沒處理這個),(Tessellation, Geometry Shader先不管),然後再將xyzwndc=xyzwclip/wclipxyzw_{ndc}=xyzw_{clip}/w_{clip},再見xyzwndcxyzw_{ndc}轉到xyzwindowxyz_{window},ndc的xyz都是[-1,1]的數值範圍了,映射到window的viewport=x,y,width,height,比較簡單:xwin=xviewport+(xndc0.5+0.5)widthviewport;ywin=yviewport+(yndc0.5+0.5)heightviewport;zwin=(zndcnear)/(farnear);x_{win}=x_{viewport}+(x_{ndc}*0.5+0.5)*width_{viewport};\\y_{win}=y_{viewport}+(y_{ndc}*0.5+0.5)*height_{viewport};\\z_{win}=(z_{ndc}-near)/(far-near);
    也可參考之前寫的:https://blog.csdn.net/linjf520/article/details/95770635

下面就是最終在窗口的座標(Window Coordinates)
在這裏插入圖片描述

Wireframe 線框

後面重構成:ShadingMode了
類似的,後面還會再添加一個:WifreframeType,可以是Point、Line,兩種。

Scissor 矩形繪製區域剔除

這個是對光柵的像素做剔除用的,就設置一個矩形(x,y,width,height)

我看很多資料教程都是將這個裁剪矩形片段剔除都是再片段着色器後處理的,不知道爲何這樣做。
而我的軟渲染器裏,是放在片段着色器前,即:光柵插值出片段的x,y座標時,就會去判斷裁剪矩形剔除。

AlphaTest alpha測試剔除

一般AlphaTest是在Scissor會後,對着色之後的片段alpha值進行AlphaTestComp的比較模式來確定剔除關係。

在我的軟渲染器裏,我是放在片段着色器後,再對alpha測試,因爲片段着色器會有可能對片段的alpha值做修改。

Cull 面向剔除

添加了個Cube,先在還沒加深度測試,所以在下面的GIF可以看到我在切換Cull爲Off時,會有一些背面的信息畫在了正面上

Cull的枚舉

  • Back - 背面,這是默認項
  • Front - 正面
  • Off 不剔除

FrontFace枚舉

  • Clock 順時針 默認(這兒與Unity一樣,OpenGL默認的是逆時針)
  • CounterClock 逆時針
    在這裏插入圖片描述

Camera 相機封裝

簡單寫了個類似Unity的Camera屬性的封裝類。
在Camera屬性中可以調整對應的屬性。
屬性分類:

  • look at 是需要lookat時的數據
  • proj-both 是透視與正交的兩者公共屬性
  • proj-ortho 是正交的屬性
  • proj-perspective 是透視的數字那個
  • transform 是該Camera的視圖矩陣、投影矩陣,可以對封裝的GameObject對象使用變換
  • view 是視圖矩陣相關的參數
  • Viewport 是窗口座標映射,將NDC座標映射到Window Coordinates映射中的x,y偏移與w,h寬高

在這裏插入圖片描述

GameObject

封裝了一個簡單易用的GameObject,沒去寫Component系統。
因爲驗證渲染功能,後續有空可完善一下

Mesh

簡單封裝了Mesh對象,有點類似Unity的Mesh也是包含一下內容

  • vertices[] 頂點(現在有用到
  • triangles[] 三角形索引(也叫indices,有用到
  • uvs[] 採樣紋理時使用的uv座標,暫時沒用到
  • colors[] 頂點顏色,暫時沒用到
  • normals[] 法線,暫時沒用到(現在暫時是使用到三角面的法線,而不是頂點法線,頂點法線還要除了挺多東西的,特別是需要處理共享到多個面在的頂點的法線值,你需要處理法線的角度混合權重)
  • tangents[] 切線,暫時沒用到(後續處理紋理空間座標是需要使用,如切線空間的法線貼圖,等)

Camera正交投影

在這裏插入圖片描述

深度測試、ShadingMode

2019.07.19,這些天有事,斷斷續續的思路,乾脆過一天再寫,添加了深度測試

深度

在下面兩Cube種,的交錯的位置,如果沒有深度測試的話,那麼效果看起來還是2D的繪製順序一樣,有了深度測試後,我們就只要管理繪製種類隊列就可以了。

我這兒的深度測試類似Early-Z,在FragmentShader之前就測試了,我這兒是再片段生成的z深度值時,就立馬判斷深度測試。不通過的都標記一下discard丟棄掉。
後面再封裝:如果使用了Early-Z就不會在走正常的FragmentShader後的DepthTest。

這裏更正一下,Early-Z其實不是這樣的,它的思想是:想用一個簡單的vs與fs,將不透明物體的深度先繪製到深度緩存,然後在會到正常的渲染流程,這樣一來,正常的渲染流程中,凡是深度不等於當前深度緩存的值,都直接剔除片段,好處就是不用處理深度上看不到片段,節省fs的計算量

但目前我的深度寫入,不是標準值,後面可以參考:LearnOpenGL-CN:深度值精度

DepthView=>DepthScreenDepth_{View} => Depth_{Screen}
視空間深度 轉化到 屏幕空間深度的公式如下:
a=F/(FN)a = F / (F - N)
b=NF/(NF)b = NF / (N - F)
depth=(aZ+b)/Zdepth_{屏幕空間} = (aZ + b)/ Z_{爲視空間深度}
depth=(aZ+b)/Zdepth = (aZ + b) / Z

DepthScreen=>DepthViewDepth_{Screen} => Depth_{View}
反推得 屏幕空間深度 轉化到 視圖空間深度的等於以下公式:
depth=(aZ+b)/Zdepth = (aZ + b) / Z
depth=a+b/Zdepth = a + b / Z
deptha=b/Zdepth - a = b / Z
(deptha)/b=1/Z(depth - a) / b = 1 / Z
b/(deptha)=Zb / (depth - a) = Z

DepthScreen=>DepthViewDepth_{Screen} => Depth_{View} 公式:b/(deptha)=Zb / (depth - a) = Z,代入a,b:
(NF/(NF)/(depth(F/(FN)))=Z(NF / (N - F) / (depth - (F / (F - N))) = Z

假設N=0.3, F=1000,代入N,F:
((0.3 * 1000) / (0.3 - 1000)) / (depth - (1000 / (1000 - 0.3))) = Z
(300 / (-999.7)) / (depth - (1000 / 999.7)) = Z
-0.3000900270081024 / (depth - 1.000300090027008)= Z

假設depth = 0.5,代入depth
-0.3000900270081024 / (0.5 - 1.000300090027008) = Z
-0.3000900270081024 / -0.500300090027008 = Z
0.5998200539838049 = Z
當depth(屏幕空間) = 0.5,Z(Z爲視空間深度) = 0.5998200539838049
那麼屏幕空間的depth就是要輸入緩存的值。

ShadingMode

這個設計與Unity差不多。

  • Shaded 就是普通着色
  • Wireframe 線框模式
  • ShadedAndWireframe 就是既有着色,又有線框
    在這裏插入圖片描述

光照

2019.07.19 簡單添加了方向光光照,我們下面還顯示法線的調試功能,便於調試光照
下面是添加了半蘭伯特光照LightDotNormal*0.5+0.5
現在我是直接添加到逐像素處理的硬編碼

後續我會再添加一個可編程的VertexShader與FragmentShader
然後我會將VertexData數據都封裝到Mesh類中,每幀都計算好對應Shader需要用的數據並上傳到Renderer中(對於OpenGL或Dx或Vulkan就是上傳到Graphicis hardware圖形硬件中,即:顯卡),以供後面的Shader階段使用
在這裏插入圖片描述

DepthOffset 深度偏移

類似Unity中的ShaderLab裏的Offset [factor, unit]

  • DepthOffset On/Off控制啓用選項
  • DepthOffsetFactor 控制Offset的factor因子值
  • DepthOffsetUnit 控制Offset的Unit值

DepthOffset一般用於解決z-fighting。
z-fighting是由於浮點數據精度有限導致的,因爲不同多邊形,同再同一平面(共面)圖元繪製時寫入深度緩存的浮點精度誤差不同導致的。
這個我之前看到好像是Nv有優化方案,就是硬件會處理同平面斜率的數據採樣保存,這樣不同圖元的深度值的精度至少是一樣的。

下面顏色如何解決z-fighting。
先來製造z-fighting,如下圖
在這裏插入圖片描述
解決方案就是類似Unity ShaderLab中的Offset -1, -1
對於我們自己封裝的使用方式類似,三行代碼就OK了

renderer.State.DepthOffset = DepthOffset.On;
renderer.State.DepthOffsetFactor = -1;
renderer.State.DepthOffsetUnit = -1;

效果如下圖
在這裏插入圖片描述

不過DepthOffset方式也有問題的,會影響後續的深度比較的精準度,特別是一些圖元交錯在一起時。
DepthOffset瞭解:

  • https://blog.csdn.net/linjf520/article/details/94596104
  • https://blog.csdn.net/linjf520/article/details/94596764

(其他的功能正在添加中,因爲對OpenGL不熟悉,在一邊看書,一邊寫程序)

Programmable Piepine Shader 可編程管線階段的着色器

2019.07.23更新添加了:可編程管線的VertexShader、FragmentShader(還有很多其他的功能,後續再介紹吧)
可編程階段,花了一些時間來設計,使用C# 反射機制來實現。
可動態加載,dll來實現shader,目前我只實現了VertexShader、FragmentShader。

這次加了可編程着色器後,幀數大大降低,但我們可以將鏡頭拉遠,讓片段少一些,稍微還是可以看到效果的。可見GPU幫CPU分擔了多少計算量。

特別是GPU的,縱向管線,與橫向並行,都是CPU無法替代的。

但這兒是以研究爲目的而設計。

先看看一個着色器代碼,我將着色器代碼放到了另一個類庫項目,編譯成.dll。
然後共主工程運行時,實時加載(其實我也可以使用C#的編譯器實時編譯一段C#爲bytes之類的,這樣就可以把C#當作腳本來加載,並編譯了)到Assembly。

先看看完成的着色器代碼,包含了頂點、片段着色器。

// jave.lin 2019.07.21
using RendererCommon.SoftRenderer.Common.Attributes;
using RendererCommon.SoftRenderer.Common.Shader;
using SoftRenderer.Common.Mathes;

namespace SoftRendererShader
{
    [VS]
    public class VertexShader : ShaderBase
    {
        [Name] public static readonly string Name = "MyTestVSShader";
        [NameHash] public static readonly int NameHash = NameUtil.HashID(Name);

        /* ==========Uniform======== */
        [Uniform] public Matrix4x4 MVP;
        [Uniform] public Matrix4x4 M;

        /* ==========In======== */

        [In] [Position] public Vector4 inPos;
        [In] [Texcoord] public Vector2 inUV;
        [In] [Color] public ColorNormalized inColor;
        //[In] [Normal] public Vector3 inNormal;

        /* ==========Out======== */

        [Out] [SV_Position] public Vector4 outPos;
        [Out] [Texcoord] public Vector2 outUV;
        [Out] [Color] public ColorNormalized outColor;
        //[Out] [Normal] public Vector3 outNormal;
        
        public VertexShader(BasicShaderData data) : base(data)
        {
        }

        [Main]
        public override void Main()
        {
            var shaderData = Data as ShaderData;

            outPos = MVP * inPos;
            outUV = inUV;
            outColor = inColor;
            //outNormal = inNormal;
        }
    }

    [FS]
    public class FragmentShader : ShaderBase
    {
        [Name]
        public static readonly string Name = "MyTestFSShader";
        [NameHash]
        public static readonly int NameHash = NameUtil.HashID(Name);

        //[In] [Position] public Vector4 inPos;
        [In] [Texcoord] public Vector2 inUV;
        [In] [Color] public ColorNormalized inColor;
        //[In] [Normal] public Vector3 inNormal;

        [Out] [SV_Target] public ColorNormalized outColor;

        public FragmentShader(BasicShaderData data) : base(data)
        {
        }

        [Main]
        public override void Main()
        {
            //1
            //var shaderData = Data as ShaderData;
            //
            //Vector3 lightDir = shaderData.LightPos[0];
            //float LdotN = Vector3.Dot(lightDir, inNormal);
            //// tex2D(tex, uv)
            //
            //outColor = inColor * LdotN;

            //2
            outColor = inColor;
            //outColor = new ColorNormalized(inUV.x, inUV.y, 0, 1);
        }
    }
}

再來詳細說明。
下面是頂點着色器的示例。

VertexShader 頂點着色器

類定義VSAttribute、繼承ShaderBase

    [VS] public class VertexShader : ShaderBase

首先是類定義加了一個VSAttribute。
標識這個類是一個頂點着色器。

你也在該shader.dll工程多添加幾個VS的類,共主工程GameObject的Material(沒錯,我有簡單的寫了個Material)切換,調用不同的Shader。

着色器名稱、哈希碼

        [Name] public static readonly string Name = "MyTestVSShader";
        [NameHash] public static readonly int NameHash = NameUtil.HashID(Name);

這兩個值分別是在Shader.dll加載後到ShaderLoaderMgr加載器後,外部可以在ShaderLoaderMgr.Create(shaderName)或是Create(shaderHash)的方式來創建Shader對象。

然後ShaderProgram對象在.SetShader(ShaderType.VertexShader, 你的Shader對象)即可。

外部調用,如下代碼:

            renderer.ShaderData = shaderData = new ShaderData(1);

            renderer.ShaderMgr.Load("Shaders/SoftRendererShader.dll");

            var vs_shaderName = "MyTestVSShader";
            var vs_shaderHash = vs_shaderName.GetHashCode();

            var fs_shaderName = "MyTestFSShader";
            var fs_shaderHash = fs_shaderName.GetHashCode();

            var vsShader = renderer.ShaderMgr.CreateShader(vs_shaderHash);
            var fsShader = renderer.ShaderMgr.CreateShader(fs_shaderHash);

            gameObjs[0].Material = new Material(vsShader, fsShader);
            gameObjs[1].Material = new Material(vsShader, fsShader);

Uniform 數據

        [Uniform] public Matrix4x4 MVP;
        [Uniform] public Matrix4x4 M;

使用[UniformAttribute]標記的字段,都是該shader的Uniform數據(我這兒的Uniform也是可以在shader運行中更改的,不想OpenGL之類的,你對Uniform設置了,可能都會編譯不通過)。Uniform數據通常是外部傳進來的多個shader之間通用的數據,這個是針對但個Shader類的數據,我們也封裝了一個是多個Shader對象都可以共享訪問的數據有點類似Uniform block:是構造函數傳進來的BasicShaderData data數據對象。

類似Uniform block的BaseShaderData

這個數據對象在外部可以使用Renderer.ShaderData來設置,也可以重置成你想要的類。

例如,我們的相機位置,燈光位置,我們都可以放這裏,這樣多個shader之間都可以拿到這些全局的共享數據。

InAttribute的輸入數據對象

        [In] [Position] public Vector4 inPos;
        [In] [Texcoord] public Vector2 inUV;
        [In] [Color] public ColorNormalized inColor;
        //[In] [Normal] public Vector3 inNormal;

我將Normal法線的字段註釋了。
因爲我還沒在MeshRenderer(我又簡單的寫了個類)中實時將法線、切線計算並傳如到頂點緩存(--!沒錯,我又寫了個VertexBuffer,還有IndexBuffer,總之寫了好多個類,--!)

除了法線沒有傳進來,我將Positionn得座標,還有Texcoord的紋理座標和Color顏色都傳進來了。

Out輸出數據對象

        [Out] [SV_Position] public Vector4 outPos;
        [Out] [Texcoord] public Vector2 outUV;
        [Out] [Color] public ColorNormalized outColor;

有輸入,還得有輸出數據。
同樣的我也寫了個SV_Position,Texcoord,Color的三個數據輸出。

SV_Position是給PrimitiveAssembly階段使用的,PrimitiveAssemly我沒有封裝類,因爲比較簡單,就是對IndexBuffer的遍歷,取到對應頂點,組合成對應PolygonMode的圖元。

有了圖元,接下來,就是將圖元光柵化,Texcoord,Color是在Rasterizer(這個類好早之前有了,不過也是重構最多次的類)光柵器中插值用的數據。(深度值是內部固有的插值數據,所以我是不在頂點着色器公開的,但在片段階段中後期我會封裝一下,可以在片段階段訪問到深度值)

最後是我們的Main函數

着色器Main函數

所有着色器都有Main函數,而且需要給Main函數添加[Main]的Attribute。

如果一個頂點着色器沒有Main函數(基類不算,我的加載器有判斷),或沒有[Out][SV_Position]的輸出數據,ShaderLoaderMgr都會報錯提示的。還有同一個字段的Attribute也不能亂加,如,有個字段加了[In]了,這時你又加上了[Out],ShaderLoaderMgr也會在加載時提示報錯。

Main函數就是我們主要的運算邏輯了。

        [Main]
        public override void Main()
        {
            outPos = MVP * inPos;
            outUV = inUV;
            outColor = inColor;
            //outNormal = inNormal;
        }

上面的Main函數我們可以看到非常簡單。
只是將對象空間下的inPos變換到裁剪空間下的outPos。
其他量個紋理座標,與顏色就是直接賦值就完了。

另外,我們的Attribute定義都可以加上一個數字,類似OpenGL中的佈局限定符的location值。

目前只有以下這幾個Attribute是有location值的:(所有location都是0~7的值,意思最多8個寄存器的概念)

  • [Color(location)]
  • [Texcoord(location)]
  • [Normal(location)]
  • [SV_Target(location)]

可以看到我們之類的Shader中的Color、Texcoord都沒加上location的定義,所以默認就是0的location值。

SV_Target的location控制片段着色器輸出到那個RT對象(後面再實現MRT)。其他的location是用於控制類似寄存器的區別的概念。

FragmentShader 片段着色器

片段着色器的我就不多介紹了,大部分與定點着色器相同,注意一下幾點:

  • 類定義使用[FS]的Attribute
  • 必須要有[Out][SV_Target]的輸出
    其他都差不多的。

運行效果

在上面的代碼下,運行情況是:(我使用的GIF錄製軟件輸出的色域比較小,如果輸出真彩色,那麼文件過大,導致CSDN博客無法上傳該GIF,所以壓縮了,顏色表小(色域小),看起來就有馬賽克,其實我自己電腦上運行時很平滑的顏色過渡的。)
在這裏插入圖片描述
然後我們調整一下代碼,用顏色用UV值來向顯示,只是調整FS中的代碼即可:

        [Main]
        public override void Main()
        {
            //outColor = inColor;
            outColor = new ColorNormalized(inUV.x, inUV.y, 0, 1);
        }

該完代碼後,記得重新編譯一下(如果真的有空,我再將C#得Roslyn編輯器拿來實時將*.cs源代碼文件編譯成XXXShader.dll或是bytes,在加載),再用uv座標顯示RG通道,以下是運行效果:
在這裏插入圖片描述

Shader Passes 沒去封裝

Pass的實現還是需要寫比較多的代碼,雖然也是可以實現的

後續還有很多其他功能,留着以後有空再寫了。

Texture Perspective Mapping 紋理投影映射

2019.07.27,但是效果失敗了,如下描述
我按照了《3D遊戲與計算機圖形學的中數學方法》的第66~68頁的內容處理了。
也參考了一下的連接:

步驟:

  • ClipPos 2 NDC Pos時,我將ClipPos.w存到了NDC.w中
  • 然後NDC 2 Window Pos時,再將WindowPos.w = NDCPos.w
  • 上面保留的WindowPos.w用來在生成片段是對掃描leftFrag到rightFrag生成片段的頂點輸出數據透視校正插值處理:
    • var invZ0 = 1 / leftFrag.pos.w;
    • var invZ1 = 1 / rightFrag.pos.w;
    • 然後在新生成的片段的z求出來,interpolatedFrag.z = 1 / Mathf.lerp(invZ0, invZ1, t);
    • 然後對所有的頂點屬性,我們重點是對UV屬性插值,例如:uv = interpolatedFrag.z * Mathhf.lerp(leftFrag.uv * invZ0, rightFrag.uv * invZ1, t);
    • 但是遺憾的是,結果還是不對,希望能有大神指點一下,如下效果圖

爲了測試,使用了修改FragmentShader的方式來生成程序紋理的橫條紋理,這樣就可以測試紋理透視校正映射到底有沒起到作用,代碼如下:

        [Main]
        public override void Main()
        {
            var v = inUV.y * 100;
            var times = (int)(v / 5);
            if (times % 2 == 0) outColor = ColorNormalized.red;
            else outColor = ColorNormalized.green;
        }

在這裏插入圖片描述

解決紋理投影校正的問題

2019.08.11 我倒回來修復這個問題了,所以寫了另一個更精簡的渲染器,後面會更新到這個功能中

C# 實現精簡版的柵格化渲染器 - 代碼很精簡,修復了投影校正的問題

Texture Wrap Mode 紋理包裹模式

2019.07.27 還是把紋理加上吧,透視校正插值的問題,有面再處理了
在FragmentShader中需要設置Texture,Sampler暫時不提供給外部設置屬性(下面演示是直接在FS中設置sampler屬性的)

外部設置紋理

            var tex_bmp = new Bitmap("Images/tex.jpg");
            var tex = new Texture2D(tex_bmp);
            fsShader.ShaderProperties.SetUniform("mainTex", tex);

FragmentShader添加採樣處理

這裏就沒去封裝TextureUnit(紋理單元,是GPU中有限的紋理處理資源之一)。
直接就在shader中聲明sampler與uniform Texture2D共外部設置。
然後shader想怎麼寫就怎麼寫了。

		...
        [Uniform] public Texture2D mainTex;
        public Sampler2D sampler;
        ...
        [Main]
        public override void Main()
        {
            outColor = tex2D(sampler, mainTex, inUV);
        }

下面是運行效果
在這裏插入圖片描述

下面我們將UV座標都放大一倍,這樣纔可以看出其他WrapMode的區別

            var uvs = new Vector2[vertices.Length];
            var uvScale = 2.0f;
            for (int i = 0; i < vertices.Length; i+=4)
            {
                uvs[i + 0] = new Vector2(0, 0) * uvScale;    // 0
                uvs[i + 1] = new Vector2(1, 1) * uvScale;    // 1
                uvs[i + 2] = new Vector2(0, 1) * uvScale;    // 2
                uvs[i + 3] = new Vector2(1, 0) * uvScale;    // 3
            }

Clamp

在這裏插入圖片描述

Repeat

在這裏插入圖片描述

Mirror

在這裏插入圖片描述

MirrorOnce

在這裏插入圖片描述

RepeatX | MirrorY

在這裏插入圖片描述

WrapMode可以任意搭配
定義:

    [Flags]
    [Description("紋理座標包裹模式(紋理座標超過1,或小於0時,如何處理這些邊界座標)")]
    public enum SampleWrapMode
    {
        Clamp = 1,              // Clamp X,Y
        Repeat = 2,             // Repeat X,Y
        Mirror = 4,             // Mirror X,Y
        MirrorOnce = 8,         // Mirror Once X,Y
        ClampX = 16,
        ClampY = 32,
        RepeatX = 64,
        RepeatY = 128,
        MirrorX = 256,
        MirrorY = 512,
        MirrorOnceX = 1024,
        MirrorOnceY = 2048,
    }

Sampler2D - WrapMode 的算法

Sampler2D - WrapMode的算法比較簡單
主要代碼是:

    [TypeConverter(typeof(ExpandableObjectConverter))]
    [Description("採樣器")]
    public struct Sampler2D
    {
        public SampleFilterMode filterMode;
        public SampleWrapMode wrapMode;

        public ColorNormalized Sample(Texture2D tex, float u, float v)
        {
            Wrap(ref u, ref v);

            var x = u * (tex.Width - 1);
            var y = v * (tex.Height - 1);

            return Filter(tex, x, y);
        }

        private void Wrap(ref float u, ref float v)
        {
            WrapU(ref u);
            WrapV(ref v);
        }

        private void WrapU(ref float u)
        {
            if ((wrapMode & SampleWrapMode.Clamp) != 0)
            {
                u = Mathf.Clamp(u, 0, 1);
            }
            else if ((wrapMode & SampleWrapMode.Repeat) != 0)
            {
                u %= 1;
                if (u < 0) u = 1 + u;
            }
            else if ((wrapMode & SampleWrapMode.Mirror) != 0)
            {
                var i = (int)u;
                u %= 1;
                if (u > 0)
                {
                    if (i % 2 == 0)
                    {
                        // noops
                    }
                    else
                    {
                        u = 1 - u;
                    }
                }
                else
                {
                    if (i % 2 == 0)
                    {
                        u = -u;
                    }
                    else
                    {
                        u = -u;
                        u = 1 - u;
                    }
                }
            }
            else if ((wrapMode & SampleWrapMode.MirrorOnce) != 0)
            {
                u = Mathf.Clamp(u, -1, 1);
                if (u < 0) u = -u;
            }
            else if ((wrapMode & SampleWrapMode.ClampX) != 0)
            {
                u = Mathf.Clamp(u, 0, 1);
            }
            else if ((wrapMode & SampleWrapMode.RepeatX) != 0)
            {
                u %= 1;
                if (u < 0) u = 1 + u;
            }
            else if ((wrapMode & SampleWrapMode.MirrorX) != 0)
            {
                var i = (int)u;
                u %= 1;
                if (u > 0)
                {
                    if (i % 2 == 0)
                    {
                        // noops
                    }
                    else
                    {
                        u = 1 - u;
                    }
                }
                else
                {
                    if (i % 2 == 0)
                    {
                        u = -u;
                    }
                    else
                    {
                        u = -u;
                        u = 1 - u;
                    }
                }
            }
            else if ((wrapMode & SampleWrapMode.MirrorOnceX) != 0)
            {
                u = Mathf.Clamp(u, -1, 1);
                if (u < 0) u = -u;
            }
            else
            {
                u = Mathf.Clamp(u, 0, 1);
            }
        }

        private void WrapV(ref float v)
        {
            if ((wrapMode & SampleWrapMode.Clamp) != 0)
            {
                v = Mathf.Clamp(v, 0, 1);
            }
            else if ((wrapMode & SampleWrapMode.Repeat) != 0)
            {
                v %= 1;
                if (v < 0) v = 1 + v;
            }
            else if ((wrapMode & SampleWrapMode.Mirror) != 0)
            {
                var i = (int)v;
                v %= 1;
                if (v > 0)
                {
                    if (i % 2 == 0)
                    {
                        // noops
                    }
                    else
                    {
                        v = 1 - v;
                    }
                }
                else
                {
                    if (i % 2 == 0)
                    {
                        v = -v;
                    }
                    else
                    {
                        v = -v;
                        v = 1 - v;
                    }
                }
            }
            else if ((wrapMode & SampleWrapMode.MirrorOnce) != 0)
            {
                v = Mathf.Clamp(v, -1, 1);
                if (v < 0) v = -v;
            }
            else if ((wrapMode & SampleWrapMode.ClampY) != 0)
            {
                v = Mathf.Clamp(v, 0, 1);
            }
            else if ((wrapMode & SampleWrapMode.RepeatY) != 0)
            {
                v %= 1;
                if (v < 0) v = 1 + v;
            }
            else if ((wrapMode & SampleWrapMode.MirrorY) != 0)
            {
                var i = (int)v;
                v %= 1;
                if (v > 0)
                {
                    if (i % 2 == 0)
                    {
                        // noops
                    }
                    else
                    {
                        v = 1 - v;
                    }
                }
                else
                {
                    if (i % 2 == 0)
                    {
                        v = -v;
                    }
                    else
                    {
                        v = -v;
                        v = 1 - v;
                    }
                }
            }
            else if ((wrapMode & SampleWrapMode.MirrorOnceY) != 0)
            {
                v = Mathf.Clamp(v, -1, 1);
                if (v < 0) v = -v;
            }
            else
            {
                v = Mathf.Clamp(v, 0, 1);
            }
        }

        private ColorNormalized Filter(Texture2D tex, float u, float v)
        {
            switch (filterMode)
            {
                case SampleFilterMode.Point:
                    return tex.Get((int)u, (int)v);
                case SampleFilterMode.Linear:
                    throw new Exception($"not implements filter mode:{filterMode}");
                case SampleFilterMode.Trilinear:
                    throw new Exception($"not implements filter mode:{filterMode}");
                default:
                    throw new Exception($"not implements filter mode:{filterMode}");
            }
        }

        public ColorNormalized Sample(Texture2D tex, Vector2 uv)
        {
            return Sample(tex, uv.x, uv.y);
        }
    }

WrapMode 主要是Mirror的需要講一下

其他都比它簡單,使用圖解具象化一下:value與mirror-value值與的變化關係
先是UV座標:
在這裏插入圖片描述
然後是value與mirror-value的關係圖解:
在這裏插入圖片描述
從圖中,我們可以看到一個規律:

  • 偶:升
  • 奇:降

無論正、負數,偶、奇數,可以發現他們都在0f~1f的值,所以一開始就需要先摺疊一下:
mirrorvalue=value%1f;mirror-value=value\%1f;

但是正負之間有所區別
正數的處理:

  • 偶數:直接摺疊即可:因爲前面有處理了:mirrorvalue=value%1f;mirror-value=value\%1f;所以這裏啥也不用處理;
  • 奇數:反向:mirrorvalue=1fvalue;mirror-value=1f-value;

負數的處理:

  • 偶數:取正數:mirrorvalue=value;mirror-value=-value;或是mirrorvalue=abs(value);mirror-value=abs(value);
  • 奇數:取正數後,再反向:value=value;mirrorvalue=1value;value=-value;mirror-value=1-value;

在上面完整代碼中也可以看到Mirror(X/Y)的處理就是這樣的。

最後來個紋理、頂點顏色的顯示;另一個是開了疊加混合(開了混合後發現邊框的片段有寫重複,後續優化)

            var c = new ColorNormalized(inUV.x, inUV.y, 1, 1);
            outColor = tex2D(sampler, mainTex, inUV) + c; // * c;

在這裏插入圖片描述在這裏插入圖片描述

Normal、Tangent 法線、切線

對Mesh類添加了:計算法線、切線的函數CaculateNormalAndTangent(),這是簡單的計算,沒有計算共用點的權重應用

    [TypeConverter(typeof(ExpandableObjectConverter))]
    [Description("網格對象")]
    public class Mesh
    {
        public Vector3[] vertices;                              // 頂點座標
        public int[] triangles { get; set; }                    // 頂點索引
        public Vector3[] normals { get; set; }                  // 頂點法線
        public Vector3[] tangents { get; set; }                 // 頂點切線
        public Vector2[] uv { get; set; }                       // 頂點uv
        public ColorNormalized[] colors { get; set; }           // 頂點顏色

        public void CaculateNormalAndTangent() // 計算法線與切線
        {
            if (normals == null || normals.Length != vertices.Length) normals = new Vector3[vertices.Length];
            if (tangents == null || tangents.Length != vertices.Length) tangents = new Vector3[vertices.Length];

            var len = triangles.Length;
            for (int i = 0; i < len; i += 3)
            {
                var idx1 = triangles[i];
                var idx2 = triangles[i + 1];
                var idx3 = triangles[i + 2];
                var v1 = vertices[idx1];
                var v2 = vertices[idx2];
                var v3 = vertices[idx3];

                var tangent = v2 - v1;
                var bitangent = v3 - v1;
                var normal = tangent.Cross(bitangent);

                normal.Normalize();
                tangent.Normalize();

                normals[idx1] = normal;
                normals[idx2] = normal;
                normals[idx3] = normal;

                tangents[idx1] = tangent;
                tangents[idx2] = tangent;
                tangents[idx3] = tangent;
            }
        }
    }

Ambient、Diffuse、Specular 環境光、漫反射、高光

看看加入了法線之後的光照shader處理

// jave.lin 2019.07.21
using RendererCommon.SoftRenderer.Common.Attributes;
using RendererCommon.SoftRenderer.Common.Shader;
using SoftRenderer.Common.Mathes;
using System.ComponentModel;

using Color = SoftRenderer.Common.Mathes.ColorNormalized;

namespace SoftRendererShader
{
    [VS]
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class VertexShader : ShaderBase
    {
        [Name] public static readonly string Name = "MyTestVSShader";
        [NameHash] public static readonly int NameHash = NameUtil.HashID(Name);

        /* ==========Uniform======== */
        [Uniform] public Matrix4x4 MVP;
        [Uniform] public Matrix4x4 M;
        [Uniform] public Matrix4x4 M_IT;

        /* ==========In======== */

        [In] [Position] public Vector4 inPos;
        [In] [Texcoord] public Vector2 inUV;
        [In] [Color] public Color inColor;
        [In] [Normal] public Vector3 inNormal;
        //[In] [Tangent] public Vector3 inTangent;

        /* ==========Out======== */

        [Out] [SV_Position] public Vector4 outPos;
        [Out] [Position] public Vector4 outWorldPos;
        [Out] [Texcoord] public Vector2 outUV;
        [Out] [Color] public Color outColor;
        [Out] [Normal] public Vector3 outNormal;
        //[Out] [Tangent] public Vector3 outTangent;

        public VertexShader(BasicShaderData data) : base(data)
        {
        }

        [Main]
        public override void Main()
        {
            outPos = MVP * inPos;
            outWorldPos = M * inPos;
            outUV = inUV;
            outColor = inColor;
            outNormal = M_IT * inNormal;
            //outTangent = M_IT * inTangent;
        }
    }

    [FS]
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class FragmentShader : FSBase
    {
        [Name] public static readonly string Name = "MyTestFSShader";
        [NameHash] public static readonly int NameHash = NameUtil.HashID(Name);

        [Uniform] public Texture2D mainTex;
        public Sampler2D sampler;

        [In] [SV_Position] public Vector4 inPos;
        [In] [Position] public Vector4 inWorldPos;
        [In] [Texcoord] public Vector2 inUV;
        [In] [Color] public Color inColor;
        [In] [Normal] public Vector3 inNormal;
        //[In] [Tangent] public Vector3 inTangent;

        [Out] [SV_Target] public Color outColor;

        public FragmentShader(BasicShaderData data) : base(data)
        {
            //sampler.wrapMode = SampleWrapMode.Clamp;
            //sampler.wrapMode = SampleWrapMode.Repeat;
            //sampler.wrapMode = SampleWrapMode.Mirror;
            //sampler.wrapMode = SampleWrapMode.MirrorOnce;
            //sampler.wrapMode = SampleWrapMode.RepeatX | SampleWrapMode.MirrorOnceY;
            //sampler.wrapMode = SampleWrapMode.RepeatY | SampleWrapMode.MirrorOnceX;
            //sampler.wrapMode = SampleWrapMode.ClampX | SampleWrapMode.MirrorY;
            //sampler.wrapMode = SampleWrapMode.ClampY | SampleWrapMode.MirrorX;
            //sampler.wrapMode = SampleWrapMode.ClampX | SampleWrapMode.RepeatY;
            //sampler.wrapMode = SampleWrapMode.ClampY | SampleWrapMode.RepeatX;
            //sampler.wrapMode = SampleWrapMode.RepeatX | SampleWrapMode.MirrorY;
            //sampler.wrapMode = SampleWrapMode.RepeatY | SampleWrapMode.MirrorX;
        }

        [Main]
        public override void Main()
        {
            //1
            var shaderData = Data as ShaderData;
            //
            //Vector3 lightDir = shaderData.LightPos[0];
            //float LdotN = Vector3.Dot(lightDir, inNormal);
            //// tex2D(tex, uv)
            //
            //outColor = inColor * LdotN;

            //2
            //outColor = inColor;
            //3
            //if (inUV.x >= 0 && inUV.x <= 0.25f) outColor = ColorNormalized.red;
            //else if (inUV.x > 0.25f && inUV.x <= 0.5f) outColor = ColorNormalized.green;
            //else if (inUV.x > 0.5f && inUV.x <= 0.75f) outColor = ColorNormalized.blue;
            //else outColor = ColorNormalized.yellow;
            // 4
            //var v = inUV.y * 100;
            //var times = (int)(v / 5);
            //if (times % 2 == 0) outColor = ColorNormalized.red;
            //else outColor = ColorNormalized.green;
            // 5
            //outColor = new ColorNormalized(inUV.x, inUV.y, 0, 1);
            // 6
            //outColor = sampler.Sample(mainTex, inUV);
            // 7
            //outColor = tex2D(sampler, mainTex, inUV);
            // 8 alpha test in here
            //var c = new ColorNormalized(inUV.x, inUV.y, 0, 1);
            //outColor = tex2D(sampler, mainTex, inUV) + c; // * c;
            //var b = outColor.r + outColor.g + outColor.b;
            //b *= 0.3f;
            //if (b < 0.9f) discard = true;

            // diffuse
            var lightDir = shaderData.LightPos[0].xyz;
            var LdotN = lightDir.Dot(inNormal);// * 0.5f + 0.5f;
            var diffuse = (1 - tex2D(sampler, mainTex, inUV)) * (LdotN * 0.5f + 0.5f) * inColor;
            // specular
            var viewDir = shaderData.CameraPos.xyz - inWorldPos.xyz;
            var specular = Color.zero;
            // specular 1
            // 高光也可以使用:光源角與視角的半角來算
            if (LdotN > 0)
            {
                var halfAngleDir = (lightDir + viewDir).normalized;
                var HdotN = max(0, halfAngleDir.Dot(inNormal));
                HdotN = pow(HdotN, 80f);
                specular = shaderData.LightColor[0] * HdotN;
            }
            // specular 2
            //var reflectDir = reflect(-lightDir.xyz, inNormal);
            //var RnotV = reflectDir.Dot(viewDir);
            //var specular = shaderData.LightColor[0] * RnotV;

            // ambient
            var ambient = shaderData.Ambient;

            outColor = diffuse + specular + ambient;

            // test
            //outColor.rgb = inNormal * 0.5f + 0.5f;
        }
    }
}

運行效果

在這裏插入圖片描述
我們運行效果使用的是正交投影,因爲透視投影有校正的問題。

法線添加了光照之後,法線柵格化的像素有問題,不知道是否有冗餘的片段的問題。

ShowNormalLines 顯示法線

2019.08.01
這次增加法線的方向的漸變,源點爲藍色,方向爲白色,如下GIF:
在這裏插入圖片描述

ShowTBN 將ShowNormalLines 該爲顯示TBN

2019.08.01
因爲有個法線錯亂的問題,困擾很久了。不得不把TBN顯示出來調試用。
在這裏插入圖片描述

Camera Control 增加控制鏡頭的方式

2019.08.03
控制方式我仿Unity的方式,不過有時會有萬向鎖的問題。
下面值演示其中一種:鼠標右鍵+w,s,a,d鍵的方式
在這裏插入圖片描述

Simple pritimive clip 增加了簡單的圖元裁剪

2019.08.03
這裏我的圖元裁剪是整個圖元裁剪的方式
在上面的鏡頭控制中,可以看到,圖元的其中一個頂點只要超出了相機視椎體範圍,都會被裁剪掉,改而顯示成線框內容。

Load DIY Model *.m 加載自定義模型*.m文件

2019.08.03
*.m文件是我在Unity中使用腳本將網格類Mesh.IsReadable,讀取出來的模型。
如果Mesh.IsReadable==false的將讀取不了。

下面是加載了Unity中的Sphere球體。我將它表面顯示出法線來。
在這裏插入圖片描述

Export & Load Mesh.isReadable==false 導出與加載Mesh.isReadable==false的模型

2019.08.03
下面這個是Unity官方自帶的例子中的模型
好卡,頂點數挺多的

導出Mesh.isReadable==false的網格之前,先執行這個腳本的applied configuration。
腳本在這:https://github.com/javelinlin/3DSoftRenderer/blob/master/SoftRenderer/Tools/TweakMeshAssets.cs

該腳本源自與:https://answers.unity.com/questions/722507/making-mesh-non-readable.html

原理是調整了ModelImportAsset.isReadable=true,然後保存資源,再刷新資源。
雖然運行時Mesh.isReadable==false,但頂點等數據都可以獲取了。
在這裏插入圖片描述
上面是直接紋理映射的結果
下面添加光照
在這裏插入圖片描述
爲何這卡,就因爲頂點很多,才4700+個,我的CPU就受不了了。-_-!
在這裏插入圖片描述

Post-Process 添加後效處理

2019.08.04
不過代碼是臨時結構,後面再優化

AA(Anti-Aliasing) 抗鋸齒

先來看一張有鋸齒的圖
在這裏插入圖片描述
然後是抗鋸齒後
在這裏插入圖片描述

我這個抗鋸齒的思路是:(與市面上的算法可能不一致,我這個只是該是思路,具體算法,不是這樣的,可以參考我下面列出的連接,這兒我就再列出來吧:基於圖片的抗鋸齒方法(一)

  • 先查找需要處理的邊緣像素
  • 處理模糊處理

如下圖,先查找邊緣
在這裏插入圖片描述

整體運行效果
在這裏插入圖片描述

說說查找邊緣算法的思路(與描邊思路一樣):

  • 像素描邊,可以使用市面上比較多的描邊算法,如:Roberts, Prewitt, sobel
    該算法思路是判斷臨近像素的顏色差異來計算的
  • 深度描邊,我上面的就是使用這個方法
    判斷臨近像素的深度差異,如果超過了你指定的閾值,那麼就判斷爲邊緣像素
  • 深度+法線描邊,我本想用這種方式的,當時我的渲染器沒有完善架構,不方便實現
    因爲需要渲染法線紋理,後續有空完善即可實現

每種算法各有優缺點:

  • 像素描邊
    • 優點:你可以按灰度或是其他顏色通道來提取邊緣,但在同一平面內你想對某些像素鋸齒優化,就需要這種方式來處理,這是深度、或是深度+法線的方式無法替代的。
    • 缺點:容易受其他信息干擾提取的準確度,如果有一些燈光,或是透明物體對像素信息有影響,那麼可能會提取到不應該提取的像素,或是需要提取的像素卻沒提取到。
  • 深度描邊
    • 優點:這種方式就可以避免了像素描邊受燈光,或是其他影響像素內容而導致提取不準的問題
    • 缺點:在同一個多邊形的像素內部,一些棱角邊緣無法提取,因爲臨近像素都是插值過來的,比較平滑的深度,你就無法提取到,如下圖
      在這裏插入圖片描述
  • 深度+法線描邊
    • 優點:可以處理模型內部棱角邊緣無法提取的問題。因爲棱角邊緣的法線是由差異的。
    • 缺點:還是不能提取像素描邊的功能。

所以,在根據具體需求來選擇不同的描邊處理,或是組合處理(像素+深度+法線)。

抗鋸齒、描邊的算法我接觸不多,應該還有很多其他的方式。

其實我上面的算法的效果不是很好。
MSAA抗鋸齒可以看看這篇:基於圖片的抗鋸齒方法(一)

FullScreenBlur 全屏均值模糊

2019.08.04
均值模糊就比較簡單了
就是取臨近像素的值混合在一起,與上面抗鋸齒的模糊算法一樣,看看效果:
在這裏插入圖片描述

重構項目,添加FrameBuffer

2019.08.06
幀緩存主要包括:

  • ColorBuffer - 顏色緩存,必要 - 可以有多個,目前最多8個(可以自己添加多少個都可以)
  • DepthBuffer - 深度緩存,必要
  • StencilBuffer - 模板緩存,可選,目前模板功能暫時未加進來
    在這裏插入圖片描述

添加:Shader/SubShader/Pass架構

2019.08.06
前天陪家人去遊玩了一天。
今天不會來,添加了新的Shader架構。
類似Unity的ShaderLab的方式。

改得比較粗糙的方式,有空再完善。在新的架構基礎上添加了對球體模型的描邊效果。

shader整體結構的代碼接口:

// jave.lin 2019.08.06
using RendererCoreCommon.Renderer.Common.Attributes;
using RendererCoreCommon.Renderer.Common.Shader;
using System;
using System.ComponentModel;
using color = RendererCoreCommon.Renderer.Common.Mathes.Vector4;
using mat4 = RendererCoreCommon.Renderer.Common.Mathes.Matrix4x4;
using vec2 = RendererCoreCommon.Renderer.Common.Mathes.Vector2;
using vec3 = RendererCoreCommon.Renderer.Common.Mathes.Vector3;
using vec4 = RendererCoreCommon.Renderer.Common.Mathes.Vector4;

namespace RendererShader
{
    [Shader]
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class SphereShader : ShaderBase
    {
        [Name] public static readonly string Name = "SphereVertexShader";
        [NameHash] public static readonly int NameHash = NameUtil.HashID(Name);

        /* ==========Uniform======== */
        // vert
        [Uniform] public mat4 MVP;
        [Uniform] public mat4 M;
        [Uniform] public mat4 P;
        [Uniform] public mat4 M_IT;
        [Uniform] public mat4 MV_IT;
        [Uniform] public float outlineOffset;

        // frag
        [Uniform] public Texture2D mainTex;
        [Uniform] public float specularPow = 1;
        public Sampler2D sampler = default(Sampler2D);

        // 查看該shader時,先將下面的subshader, pass[n]先的IDE裏代碼摺疊起來,會清晰很多
        private class _SubShader : SubShaderExt<SphereShader>
        {
            public _SubShader(SphereShader shader) : base(shader)
            {
                passList.Add(new _PassExt(this));
                passList.Add(new _PassExt1(this));
            }
        }

        // 正常繪製模式的pass
        private class _PassExt : PassExt<_SubShader>
        {
            /* ==========In or Out======== */
            public class _VertField : FuncField
            {
                [In] [Position] public vec4 inPos;

                [In] [Out] [Texcoord] public vec2 ioUV;
                [In] [Out] [Color] public color ioColor;
                [In] [Out] [Normal] public vec3 ioNormal;
                [In] [Out] [Tangent] public vec3 ioTangent;

                [Out] [Tangent(1)] public vec3 outBitangent;

                [Out] [SV_Position] public vec4 outPos;
                [Out] [Position] public vec4 outWorldPos;

                public _VertField(Pass pass) : base(pass)
                {
                }
            }

            public class _FragField : FuncField
            {
                [In] [SV_Position] public vec4 inPos;
                [In] [Position] public vec4 inWorldPos;
                [In] [Texcoord] public vec2 inUV;
                [In] [Color] public color inColor;
                [In] [Normal] public vec3 inNormal;
                [In] [Tangent] public vec3 inTangent;
                [In] [Tangent(1)] public vec3 inBitangent;

                [Out] [SV_Target] public color outColor;
                [Out] [SV_Target(1)] public color outNormal;

                public _FragField(Pass pass) : base(pass)
                {
                }
            }

            private _VertField vertexField;
            private _FragField fragField;
            private SphereShader shader;

            public override FuncField VertField
            {
                get => vertexField;
                protected set => vertexField = value as _VertField;
            }

            public override FuncField FragField
            {
                get => fragField;
                protected set => fragField = value as _FragField;
            }

            public _PassExt(_SubShader subshader) : base(subshader)
            {
                shader = subshader.Shader_T;

                VertField = new _VertField(this);
                FragField = new _FragField(this);
            }

            public override void Attach()
            {
                shader.vert = Vert;
                shader.frag = Frag;
            }

            private void Vert()
            {
                vertexField.ioColor = color.yellow;
                vertexField.inPos.xyz += vertexField.ioNormal * shader.outlineOffset;
                vertexField.outPos = shader.MVP * vertexField.inPos;
                vertexField.outWorldPos = shader.M * vertexField.inPos;
                vertexField.ioNormal = shader.M_IT * vertexField.ioNormal;
                vertexField.ioTangent = shader.M_IT * vertexField.ioTangent;
                vertexField.outBitangent = vertexField.ioNormal.Cross(vertexField.ioTangent);
            }

            private void Frag()
            {
                var shaderData = shader.Data as ShaderData;

                // diffuse
                var lightPos = shaderData.LightPos[0];
                var lightType = lightPos.w;
                vec3 lightDir;
                if (lightType == 0) // 方向光
                    lightDir = lightPos.xyz;
                else if (lightType == 1) // 點光源
                    lightDir = (lightPos.xyz - fragField.inWorldPos.xyz).normalized;
                // intensity = max(0, 1 - distance / range);
                else
                    throw new Exception($"not implements lightType:{lightType}");
                var LdotN = dot(lightDir, fragField.inNormal);// * 0.5f + 0.5f;
                var diffuse = (tex2D(shader.sampler, shader.mainTex, fragField.inUV)) * 2 * (LdotN * 0.5f + 0.5f) * fragField.inColor;
                diffuse *= fragField.inNormal * 2;
                // specular
                var viewDir = (shaderData.CameraPos.xyz - fragField.inWorldPos.xyz);
                viewDir.Normalize();
                var specular = color.black;

                //if (LdotN > 0)
                {
                    // specular 1 - blinn-phong
                    // 高光也可以使用:光源角與視角的半角來算
                    //var halfAngleDir = (lightDir + viewDir);
                    //halfAngleDir.Normalize();
                    //var HdotN = max(0, dot(halfAngleDir, inNormal));
                    //HdotN = pow(HdotN, specularPow);
                    //specular.rgb = (shaderData.LightColor[0] * HdotN).rgb * shaderData.LightColor[0].a;
                    // specular 2 - phong
                    var reflectDir = reflect(-lightDir, fragField.inNormal);
                    var RnotV = max(0, dot(reflectDir, viewDir));
                    RnotV = pow(RnotV, shader.specularPow) * (LdotN * 0.5f + 0.5f);
                    specular.rgb = (shaderData.LightColor[0] * RnotV).rgb * shaderData.LightColor[0].a;
                }

                // ambient
                var ambient = shaderData.Ambient;
                ambient.rgb *= ambient.a;

                fragField.outColor = diffuse + specular + ambient;
                fragField.outNormal = fragField.inNormal;
            }

            public override void Dispose()
            {
                if (vertexField != null)
                {
                    vertexField.Dispose();
                    vertexField = null;
                }
                if (fragField != null)
                {
                    fragField.Dispose();
                    fragField = null;
                }
                base.Dispose();
            }
        }

        // 描邊的pass
        private class _PassExt1 : PassExt<_SubShader>
        {
            /* ==========In or Out======== */
            public class _VertField : FuncField
            {
                [In] [Position] public vec4 inPos;

                [In] [Out] [Normal] public vec3 ioNormal;

                [Out] [SV_Position] public vec4 outPos;

                public _VertField(Pass pass) : base(pass)
                {
                }
            }

            public class _FragField : FuncField
            {
                [Out] [SV_Target] public color outColor;
                [Out] [SV_Target(1)] public color outNormal;

                public _FragField(Pass pass) : base(pass)
                {
                }
            }

            private _VertField vertexField;
            private _FragField fragField;
            private SphereShader shader;

            public override FuncField VertField
            {
                get => vertexField;
                protected set => vertexField = value as _VertField;
            }

            public override FuncField FragField
            {
                get => fragField;
                protected set => fragField = value as _FragField;
            }

            public _PassExt1(_SubShader subshader) : base(subshader)
            {
                shader = subshader.Shader_T;

                VertField = new _VertField(this);
                FragField = new _FragField(this);

                State = new DrawState
                {
                    Cull = FaceCull.Front,
                    DepthWrite = DepthWrite.Off,
                };
            }

            public override void Attach()
            {
                shader.vert = Vert;
                shader.frag = Frag;
            }

            private void Vert()
            {
                //https://blog.csdn.net/linjf520/article/details/95064552#t10
                vertexField.outPos = shader.MVP * vertexField.inPos;
                var n = shader.MV_IT * vertexField.ioNormal;
                n = shader.P * n;
                vertexField.ioNormal = shader.M_IT * vertexField.ioNormal;
                vertexField.outPos.xy += n.xy * 0.1f;// * vertexField.outPos.w;
            }

            private void Frag()
            {
                fragField.outColor = color.yellow;
            }

            public override void Dispose()
            {
                if (vertexField != null)
                {
                    vertexField.Dispose();
                    vertexField = null;
                }
                if (fragField != null)
                {
                    fragField.Dispose();
                    fragField = null;
                }
                base.Dispose();
            }
        }

        public SphereShader(BasicShaderData data) : base(data)
        {
            SubShaderList.Add(new _SubShader(this));
        }
    }
}

在這裏插入圖片描述

StencilTest/Buffer 添加了模板緩存,模板測試功能

2019.08.07,今天去看胃病,沒時間寫,花了一點時間,就將之前鋪墊已久的Stencil一下就加進來了

使用Stencil來描邊

在這裏插入圖片描述

使用新的Pass方式來描邊

  • 第一次的pass先繪製一個原始模型
  • 第二次pass,先將模型按投影空間下的法線來膨脹頂點,這樣模型就會變大,再繪製背面內容。

描邊可參考:https://blog.csdn.net/linjf520/article/details/95064552#t10

效果圖如下:(右邊的圖是我對SV_Target(1)輸出了法線的貼圖,然後我顯示出來了,黃色描邊的法線顏色SV_Target(1)我沒賦值,所以默認是(0,0,0,1)的顏色:黑色)

無透視描邊

在這裏插入圖片描述

有透視描邊

在這裏插入圖片描述

StencilBuffer/Test 添加模板緩存、測試

2019.08.07 今天去醫院看老胃病了,不過還好只是花了一點時間,就將之前鋪墊已久的Stencil功能加進來了

使用Stencil來描邊

在這裏插入圖片描述

使用Stencil來遮罩

先準備一下mask圖,我使用GIMP隨便畫了一張
在這裏插入圖片描述
然後調整一下所有需要遮罩的shader,運行效果
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
GIF錄製了很多次都時後面就會花屏,不知道什麼原因
在這裏插入圖片描述

使用Stencil來鏤空

我們準備了另一個圖,也是使用GIMP來隨表畫的
在這裏插入圖片描述
運行效果,也是錄製到後面就花屏,應該是軟件的BUG
在這裏插入圖片描述

矩陣驗算工具

爲了修復法線問題,自己搞了個excel來驗算,輸入各種參數,即可立馬得到結果
文件:驗算矩陣Exxcel
如下圖:
在這裏插入圖片描述

後續製作

因爲計劃有所變動,暫時停止了渲染器開發。
但是留下了一些比較重要需要實現的內容:

  • ShadowMap 陰影實現,我就不在這個軟渲染器裏實現了,怕性能扛不上。
  • Deferred Rendering 延遲渲染
    • 延遲渲染的話可能重構比較多,因爲要兼容之前的正向渲染路徑,那麼就需要給外部公開接口可設置渲染路徑模式
  • 將軟渲染的頂點、像素着色器的計算挪到:GPU運算,也不是不可能,但是我覺得沒有必要了,當那點我熟悉了,GPGPU,或是CUDA之後,想起來了,再製作吧。

待後面有空,我會將這兩塊功能加入。

總結

(本想繼續寫下去的,但發現時間有限,還是等後續有空,一邊學習,一邊補上其他的功能。)

自己寫的這個軟渲染功能,都是一邊查資料惡補圖形知識這塊,一邊寫,很多代碼、結構寫法,爲了可讀性,沒去優化,所以優化空間還有很大。

寫的這些都是顯卡很基礎的處理,而且顯卡發展了這麼多年了。
都是經歷過各種管線優化,算法優化。

總得來說GPU 的渲染管線給CPU分擔了N多計算。
特別是GPU的各種縱向管線、橫向並行的計算單元資源,都將優化最大化了。

最後,最大感悟:數學,真的可以改變世界。可惜我沒學好。T^T, QAQ。

Project

3DSoftRenderer

References

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章