用C# Bitmap作为画布写个3D软渲染器

文章目录

  • Recoards 记录
    • 图元光栅
    • Bitmap.SetPixel优化成LockBits/UnlockBits指针操作
    • Blend
    • Projection 投影
    • Wireframe 线框
    • Scissor 矩形绘制区域剔除
    • AlphaTest alpha测试剔除
    • Cull 面向剔除
      • Cull的枚举
      • FrontFace枚举
    • Camera 相机封装
    • GameObject
    • Mesh
    • Camera正交投影
    • 深度测试、ShadingMode
    • 深度
    • ShadingMode
    • 光照
    • DepthOffset 深度偏移
    • Programmable Piepine Shader 可编程管线阶段的着色器
      • VertexShader 顶点着色器
        • 类定义VSAttribute、继承ShaderBase
        • 着色器名称、哈希码
        • Uniform 数据
        • 类似Uniform block的BaseShaderData
        • InAttribute的输入数据对象
        • Out输出数据对象
        • 着色器Main函数
      • FragmentShader 片段着色器
        • 运行效果
        • Shader Passes 没去封装
    • Texture Perspective Mapping 纹理投影映射
      • 步骤:
    • 解决纹理投影校正的问题
    • Texture Wrap Mode 纹理包裹模式
      • 外部设置纹理
      • FragmentShader添加采样处理
      • Clamp
      • Repeat
      • Mirror
      • MirrorOnce
      • RepeatX | MirrorY
      • Sampler2D - WrapMode 的算法
        • WrapMode 主要是Mirror的需要讲一下
    • Normal、Tangent 法线、切线
    • Ambient、Diffuse、Specular 环境光、漫反射、高光
      • 运行效果
    • ShowNormalLines 显示法线
    • ShowTBN 将ShowNormalLines 该为显示TBN
    • Camera Control 增加控制镜头的方式
    • Simple pritimive clip 增加了简单的图元裁剪
    • Load DIY Model *.m 加载自定义模型\*.m文件
    • Export & Load Mesh.isReadable\==false 导出与加载Mesh.isReadable==false的模型
    • Post-Process 添加后效处理
      • AA(Anti-Aliasing) 抗锯齿
      • FullScreenBlur 全屏均值模糊
    • 重构项目,添加FrameBuffer
    • 添加:Shader/SubShader/Pass架构
    • StencilTest/Buffer 添加了模板缓存,模板测试功能
      • 使用Stencil来描边
      • 使用新的Pass方式来描边
        • 无透视描边
        • 有透视描边
    • StencilBuffer/Test 添加模板缓存、测试
      • 使用Stencil来描边
      • 使用Stencil来遮罩
      • 使用Stencil来镂空
    • 矩阵验算工具
  • 后续制作
  • 总结
  • Project
  • References

用C# Bitmap作为画布写个3D软渲染器

目的为了了解,验证图形功能,加深基础知识。
写这个光栅渲染器过程,真的发现自己的数学知识不够。
会有挺多卡点。
不知道谁可以介绍一些超级基础数学知识的书本或是教程,或是学习方式。
关于:线性代数,几何变换,仿射,微积分,的就好,其实我自己也搜索过很多资料。
就是没找到好的资源,可能自己的基础太差,没法理解真正的几何意义。

Recoards 记录

图元光栅

Start 开始
今天写的栅格器,运行效果
用C# Bitmap作为画布写个3D软渲染器_第1张图片
发现对底层还是很多不了解的,要先暂停,看一下理论知识,总结差不多,再继续写

一边学习OpenGL,一边总结写到软渲染器中。
但是性能不用考虑,因为我只渲染一帧。

法线渲染一帧都挺卡的(-_-!),特别是片段生成多的时候,更加卡。。。

Bitmap.SetPixel优化成LockBits/UnlockBits指针操作

2019.07.16 优化了Bitmap的像素设置方式。
才发现底层有更好的接口来画画布。
下面是设置了一个Timer.Interval = 15ms、10ms、1ms都试过。FPS都不变。
然后我将,Draw的代码屏蔽掉,结果FPS还是一样。
估计的Timer内部的Tick事件限制频率了。
用C# Bitmap作为画布写个3D软渲染器_第2张图片

Blend

2019.07.17 添加了Blend
片段多了,性能还是上不去。-_-!

上面的GIF是旧的,因为之前没发现BitmapData的颜色指针的数据中,是BGRA而不是RGBA。
所以混合效果有些不太一样,这个问题就修复。
用C# Bitmap作为画布写个3D软渲染器_第3张图片

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 = M a t r i x p r o j e c t ⋅ M a t r i x v i e w ⋅ M a t r i x m o d e l / w o r l d Matrix_{project} \cdot Matrix_{view} \cdot Matrix_{model/world} MatrixprojectMatrixviewMatrixmodel/world组合成的
      • Clip Space Pos = M V P ⋅ O b j e c t S p a c e P o s MVP \cdot Object Space Pos MVPObjectSpacePos
  • NDC, Window Coordinates - VertexShader Post-Processing // 是VertexShader后处理阶段,一般就是Primitive Assembly,会对图元生成(根据你指定的是Point,还是Line,Triange等),对其剪切(我没处理这个),(Tessellation, Geometry Shader先不管),然后再将 x y z w n d c = x y z w c l i p / w c l i p xyzw_{ndc}=xyzw_{clip}/w_{clip} xyzwndc=xyzwclip/wclip,再见 x y z w n d c xyzw_{ndc} xyzwndc转到 x y z w i n d o w xyz_{window} xyzwindow,ndc的xyz都是[-1,1]的数值范围了,映射到window的viewport=x,y,width,height,比较简单: x w i n = x v i e w p o r t + ( x n d c ∗ 0.5 + 0.5 ) ∗ w i d t h v i e w p o r t ; y w i n = y v i e w p o r t + ( y n d c ∗ 0.5 + 0.5 ) ∗ h e i g h t v i e w p o r t ; z w i n = ( z n d c − n e a r ) / ( f a r − n e a r ) ; 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); xwin=xviewport+(xndc0.5+0.5)widthviewport;ywin=yviewport+(yndc0.5+0.5)heightviewport;zwin=(zndcnear)/(farnear);
    也可参考之前写的:https://blog.csdn.net/linjf520/article/details/95770635

下面就是最终在窗口的坐标(Window Coordinates)
用C# Bitmap作为画布写个3D软渲染器_第4张图片

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 逆时针
    用C# Bitmap作为画布写个3D软渲染器_第5张图片

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宽高

用C# Bitmap作为画布写个3D软渲染器_第6张图片

GameObject

封装了一个简单易用的GameObject,没去写Component系统。
因为验证渲染功能,后续有空可完善一下

Mesh

简单封装了Mesh对象,有点类似Unity的Mesh也是包含一下内容

  • vertices[] 顶点(现在有用到
  • triangles[] 三角形索引(也叫indices,有用到
  • uvs[] 采样纹理时使用的uv坐标,暂时没用到
  • colors[] 顶点颜色,暂时没用到
  • normals[] 法线,暂时没用到(现在暂时是使用到三角面的法线,而不是顶点法线,顶点法线还要除了挺多东西的,特别是需要处理共享到多个面在的顶点的法线值,你需要处理法线的角度混合权重)
  • tangents[] 切线,暂时没用到(后续处理纹理空间坐标是需要使用,如切线空间的法线贴图,等)

Camera正交投影

用C# Bitmap作为画布写个3D软渲染器_第7张图片

深度测试、ShadingMode

2019.07.19,这些天有事,断断续续的思路,干脆过一天再写,添加了深度测试

深度

在下面两Cube种,的交错的位置,如果没有深度测试的话,那么效果看起来还是2D的绘制顺序一样,有了深度测试后,我们就只要管理绘制种类队列就可以了。

我这儿的深度测试类似Early-Z,在FragmentShader之前就测试了,我这儿是再片段生成的z深度值时,就立马判断深度测试。不通过的都标记一下discard丢弃掉。
后面再封装:如果使用了Early-Z就不会在走正常的FragmentShader后的DepthTest。

这里更正一下,Early-Z其实不是这样的,它的思想是:想用一个简单的vs与fs,将不透明物体的深度先绘制到深度缓存,然后在会到正常的渲染流程,这样一来,正常的渲染流程中,凡是深度不等于当前深度缓存的值,都直接剔除片段,好处就是不用处理深度上看不到片段,节省fs的计算量

但目前我的深度写入,不是标准值,后面可以参考:LearnOpenGL-CN:深度值精度

D e p t h V i e w = > D e p t h S c r e e n Depth_{View} => Depth_{Screen} DepthView=>DepthScreen
视空间深度 转化到 屏幕空间深度的公式如下:
a = F / ( F − N ) a = F / (F - N) a=F/(FN)
b = N F / ( N − F ) b = NF / (N - F) b=NF/(NF)
d e p t h 屏 幕 空 间 = ( a Z + b ) / Z 为 视 空 间 深 度 depth_{屏幕空间} = (aZ + b)/ Z_{为视空间深度} depth=(aZ+b)/Z
d e p t h = ( a Z + b ) / Z depth = (aZ + b) / Z depth=(aZ+b)/Z

D e p t h S c r e e n = > D e p t h V i e w Depth_{Screen} => Depth_{View} DepthScreen=>DepthView
反推得 屏幕空间深度 转化到 视图空间深度的等于以下公式:
d e p t h = ( a Z + b ) / Z depth = (aZ + b) / Z depth=(aZ+b)/Z
d e p t h = a + b / Z depth = a + b / Z depth=a+b/Z
d e p t h − a = b / Z depth - a = b / Z deptha=b/Z
( d e p t h − a ) / b = 1 / Z (depth - a) / b = 1 / Z (deptha)/b=1/Z
b / ( d e p t h − a ) = Z b / (depth - a) = Z b/(deptha)=Z

D e p t h S c r e e n = > D e p t h V i e w Depth_{Screen} => Depth_{View} DepthScreen=>DepthView 公式: b / ( d e p t h − a ) = Z b / (depth - a) = Z b/(deptha)=Z,代入a,b:
( N F / ( N − F ) / ( d e p t h − ( F / ( F − N ) ) ) = Z (NF / (N - F) / (depth - (F / (F - N))) = Z (NF/(NF)/(depth(F/(FN)))=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 就是既有着色,又有线框
    用C# Bitmap作为画布写个3D软渲染器_第8张图片

光照

2019.07.19 简单添加了方向光光照,我们下面还显示法线的调试功能,便于调试光照
下面是添加了半兰伯特光照LightDotNormal*0.5+0.5
现在我是直接添加到逐像素处理的硬编码

后续我会再添加一个可编程的VertexShader与FragmentShader
然后我会将VertexData数据都封装到Mesh类中,每帧都计算好对应Shader需要用的数据并上传到Renderer中(对于OpenGL或Dx或Vulkan就是上传到Graphicis hardware图形硬件中,即:显卡),以供后面的Shader阶段使用
用C# Bitmap作为画布写个3D软渲染器_第9张图片

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,如下图
用C# Bitmap作为画布写个3D软渲染器_第10张图片
解决方案就是类似Unity ShaderLab中的Offset -1, -1
对于我们自己封装的使用方式类似,三行代码就OK了

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

效果如下图
用C# Bitmap作为画布写个3D软渲染器_第11张图片

不过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,所以压缩了,颜色表小(色域小),看起来就有马赛克,其实我自己电脑上运行时很平滑的颜色过渡的。)
用C# Bitmap作为画布写个3D软渲染器_第12张图片
然后我们调整一下代码,用颜色用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通道,以下是运行效果:
用C# Bitmap作为画布写个3D软渲染器_第13张图片

Shader Passes 没去封装

Pass的实现还是需要写比较多的代码,虽然也是可以实现的

后续还有很多其他功能,留着以后有空再写了。

Texture Perspective Mapping 纹理投影映射

2019.07.27,但是效果失败了,如下描述
我按照了《3D游戏与计算机图形学的中数学方法》的第66~68页的内容处理了。
也参考了一下的连接:

  • 透视校正插值(Perspective-Correct Interpolation)

  • 透视下的插值(Perspective-Correct Interpolation)

  • 透视校正插值

  • 3D图形学学习总结(十)—纹理映射透视矫正 - 这个博客直接推导公式,可以参考里头的原理

  • 只看Perspective Correct Texture Mapping部分 - 未尝试

  • 3D 图形光栅化的透视校正问题 - 未看完

步骤:

  • 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;
        }

用C# Bitmap作为画布写个3D软渲染器_第14张图片

解决纹理投影校正的问题

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);
        }

下面是运行效果
用C# Bitmap作为画布写个3D软渲染器_第15张图片

下面我们将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

用C# Bitmap作为画布写个3D软渲染器_第16张图片

Repeat

用C# Bitmap作为画布写个3D软渲染器_第17张图片

Mirror

用C# Bitmap作为画布写个3D软渲染器_第18张图片

MirrorOnce

用C# Bitmap作为画布写个3D软渲染器_第19张图片

RepeatX | MirrorY

用C# Bitmap作为画布写个3D软渲染器_第20张图片

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坐标:
用C# Bitmap作为画布写个3D软渲染器_第21张图片
然后是value与mirror-value的关系图解:
用C# Bitmap作为画布写个3D软渲染器_第22张图片
从图中,我们可以看到一个规律:

  • 偶:升
  • 奇:降

无论正、负数,偶、奇数,可以发现他们都在0f~1f的值,所以一开始就需要先折叠一下:
m i r r o r − v a l u e = v a l u e % 1 f ; mirror-value=value\%1f; mirrorvalue=value%1f;

但是正负之间有所区别
正数的处理:

  • 偶数:直接折叠即可:因为前面有处理了: m i r r o r − v a l u e = v a l u e % 1 f ; mirror-value=value\%1f; mirrorvalue=value%1f;所以这里啥也不用处理;
  • 奇数:反向: m i r r o r − v a l u e = 1 f − v a l u e ; mirror-value=1f-value; mirrorvalue=1fvalue;

负数的处理:

  • 偶数:取正数: m i r r o r − v a l u e = − v a l u e ; mirror-value=-value; mirrorvalue=value;或是 m i r r o r − v a l u e = a b s ( v a l u e ) ; mirror-value=abs(value); mirrorvalue=abs(value);
  • 奇数:取正数后,再反向: v a l u e = − v a l u e ; m i r r o r − v a l u e = 1 − v a l u e ; value=-value;mirror-value=1-value; value=value;mirrorvalue=1value;

在上面完整代码中也可以看到Mirror(X/Y)的处理就是这样的。

最后来个纹理、顶点颜色的显示;另一个是开了叠加混合(开了混合后发现边框的片段有写重复,后续优化)

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

用C# Bitmap作为画布写个3D软渲染器_第23张图片用C# Bitmap作为画布写个3D软渲染器_第24张图片

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;
        }
    }
}

运行效果

用C# Bitmap作为画布写个3D软渲染器_第25张图片
我们运行效果使用的是正交投影,因为透视投影有校正的问题。

法线添加了光照之后,法线栅格化的像素有问题,不知道是否有冗余的片段的问题。

ShowNormalLines 显示法线

2019.08.01
这次增加法线的方向的渐变,源点为蓝色,方向为白色,如下GIF:
用C# Bitmap作为画布写个3D软渲染器_第26张图片

ShowTBN 将ShowNormalLines 该为显示TBN

2019.08.01
因为有个法线错乱的问题,困扰很久了。不得不把TBN显示出来调试用。
用C# Bitmap作为画布写个3D软渲染器_第27张图片

Camera Control 增加控制镜头的方式

2019.08.03
控制方式我仿Unity的方式,不过有时会有万向锁的问题。
下面值演示其中一种:鼠标右键+w,s,a,d键的方式
用C# Bitmap作为画布写个3D软渲染器_第28张图片

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,但顶点等数据都可以获取了。
用C# Bitmap作为画布写个3D软渲染器_第29张图片
上面是直接纹理映射的结果
下面添加光照
用C# Bitmap作为画布写个3D软渲染器_第30张图片
为何这卡,就因为顶点很多,才4700+个,我的CPU就受不了了。-_-!
用C# Bitmap作为画布写个3D软渲染器_第31张图片

Post-Process 添加后效处理

2019.08.04
不过代码是临时结构,后面再优化

AA(Anti-Aliasing) 抗锯齿

先来看一张有锯齿的图
用C# Bitmap作为画布写个3D软渲染器_第32张图片
然后是抗锯齿后
用C# Bitmap作为画布写个3D软渲染器_第33张图片

我这个抗锯齿的思路是:(与市面上的算法可能不一致,我这个只是该是思路,具体算法,不是这样的,可以参考我下面列出的连接,这儿我就再列出来吧:基于图片的抗锯齿方法(一))

  • 先查找需要处理的边缘像素
  • 处理模糊处理

如下图,先查找边缘
用C# Bitmap作为画布写个3D软渲染器_第34张图片

整体运行效果
用C# Bitmap作为画布写个3D软渲染器_第35张图片

说说查找边缘算法的思路(与描边思路一样):

  • 像素描边,可以使用市面上比较多的描边算法,如:Roberts, Prewitt, sobel
    该算法思路是判断临近像素的颜色差异来计算的
  • 深度描边,我上面的就是使用这个方法
    判断临近像素的深度差异,如果超过了你指定的阈值,那么就判断为边缘像素
  • 深度+法线描边,我本想用这种方式的,当时我的渲染器没有完善架构,不方便实现
    因为需要渲染法线纹理,后续有空完善即可实现

每种算法各有优缺点:

  • 像素描边
    • 优点:你可以按灰度或是其他颜色通道来提取边缘,但在同一平面内你想对某些像素锯齿优化,就需要这种方式来处理,这是深度、或是深度+法线的方式无法替代的。
    • 缺点:容易受其他信息干扰提取的准确度,如果有一些灯光,或是透明物体对像素信息有影响,那么可能会提取到不应该提取的像素,或是需要提取的像素却没提取到。
  • 深度描边
    • 优点:这种方式就可以避免了像素描边受灯光,或是其他影响像素内容而导致提取不准的问题
    • 缺点:在同一个多边形的像素内部,一些棱角边缘无法提取,因为临近像素都是插值过来的,比较平滑的深度,你就无法提取到,如下图
      用C# Bitmap作为画布写个3D软渲染器_第36张图片
  • 深度+法线描边
    • 优点:可以处理模型内部棱角边缘无法提取的问题。因为棱角边缘的法线是由差异的。
    • 缺点:还是不能提取像素描边的功能。

所以,在根据具体需求来选择不同的描边处理,或是组合处理(像素+深度+法线)。

抗锯齿、描边的算法我接触不多,应该还有很多其他的方式。

其实我上面的算法的效果不是很好。
MSAA抗锯齿可以看看这篇:基于图片的抗锯齿方法(一)

FullScreenBlur 全屏均值模糊

2019.08.04
均值模糊就比较简单了
就是取临近像素的值混合在一起,与上面抗锯齿的模糊算法一样,看看效果:
用C# Bitmap作为画布写个3D软渲染器_第37张图片

重构项目,添加FrameBuffer

2019.08.06
帧缓存主要包括:

  • ColorBuffer - 颜色缓存,必要 - 可以有多个,目前最多8个(可以自己添加多少个都可以)
  • DepthBuffer - 深度缓存,必要
  • StencilBuffer - 模板缓存,可选,目前模板功能暂时未加进来
    用C# Bitmap作为画布写个3D软渲染器_第38张图片

添加: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));
        }
    }
}

用C# Bitmap作为画布写个3D软渲染器_第39张图片

StencilTest/Buffer 添加了模板缓存,模板测试功能

2019.08.07,今天去看胃病,没时间写,花了一点时间,就将之前铺垫已久的Stencil一下就加进来了

使用Stencil来描边

用C# Bitmap作为画布写个3D软渲染器_第40张图片

使用新的Pass方式来描边

  • 第一次的pass先绘制一个原始模型
  • 第二次pass,先将模型按投影空间下的法线来膨胀顶点,这样模型就会变大,再绘制背面内容。

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

效果图如下:(右边的图是我对SV_Target(1)输出了法线的贴图,然后我显示出来了,黄色描边的法线颜色SV_Target(1)我没赋值,所以默认是(0,0,0,1)的颜色:黑色)

无透视描边

用C# Bitmap作为画布写个3D软渲染器_第41张图片

有透视描边

StencilBuffer/Test 添加模板缓存、测试

2019.08.07 今天去医院看老胃病了,不过还好只是花了一点时间,就将之前铺垫已久的Stencil功能加进来了

使用Stencil来描边

用C# Bitmap作为画布写个3D软渲染器_第42张图片

使用Stencil来遮罩

先准备一下mask图,我使用GIMP随便画了一张
用C# Bitmap作为画布写个3D软渲染器_第43张图片
然后调整一下所有需要遮罩的shader,运行效果
用C# Bitmap作为画布写个3D软渲染器_第44张图片
用C# Bitmap作为画布写个3D软渲染器_第45张图片
用C# Bitmap作为画布写个3D软渲染器_第46张图片
GIF录制了很多次都时后面就会花屏,不知道什么原因
用C# Bitmap作为画布写个3D软渲染器_第47张图片

使用Stencil来镂空

我们准备了另一个图,也是使用GIMP来随表画的
用C# Bitmap作为画布写个3D软渲染器_第48张图片
运行效果,也是录制到后面就花屏,应该是软件的BUG
用C# Bitmap作为画布写个3D软渲染器_第49张图片

矩阵验算工具

为了修复法线问题,自己搞了个excel来验算,输入各种参数,即可立马得到结果
文件:验算矩阵Exxcel
如下图:
用C# Bitmap作为画布写个3D软渲染器_第50张图片

后续制作

因为计划有所变动,暂时停止了渲染器开发。
但是留下了一些比较重要需要实现的内容:

  • ShadowMap 阴影实现,我就不在这个软渲染器里实现了,怕性能扛不上。
    • 在我Unity实现了,重点理解思路:Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap
  • Deferred Rendering 延迟渲染
    • 延迟渲染的话可能重构比较多,因为要兼容之前的正向渲染路径,那么就需要给外部公开接口可设置渲染路径模式
  • 将软渲染的顶点、像素着色器的计算挪到:GPU运算,也不是不可能,但是我觉得没有必要了,当那点我熟悉了,GPGPU,或是CUDA之后,想起来了,再制作吧。

待后面有空,我会将这两块功能加入。

总结

(本想继续写下去的,但发现时间有限,还是等后续有空,一边学习,一边补上其他的功能。)

自己写的这个软渲染功能,都是一边查资料恶补图形知识这块,一边写,很多代码、结构写法,为了可读性,没去优化,所以优化空间还有很大。

写的这些都是显卡很基础的处理,而且显卡发展了这么多年了。
都是经历过各种管线优化,算法优化。

总得来说GPU 的渲染管线给CPU分担了N多计算。
特别是GPU的各种纵向管线、横向并行的计算单元资源,都将优化最大化了。

最后,最大感悟:数学,真的可以改变世界。可惜我没学好。T^T, QAQ。

Project

3DSoftRenderer

References

  • 用C#实现一个简易的软件光栅化渲染器 - 现在还没看,等我写完这个光栅化渲染器后,再去看看其他同学的
  • SoftRenderer&RenderPipeline(从迷你光栅化软渲染器的实现看渲染流水线)
  • 软渲染是什么
  • 用JavaScript玩转计算机图形学(一)光线追踪入门 - 后面尝试一下光线追踪的实现,不得不说miloyip真的大神
  • 移花接木—做一个简单的软件渲染器

你可能感兴趣的:(C#,理论,图形)