目的为了了解,验证图形功能,加深基础知识。
写这个光栅渲染器过程,真的发现自己的数学知识不够。
会有挺多卡点。
不知道谁可以介绍一些超级基础数学知识的书本或是教程,或是学习方式。
关于:线性代数,几何变换,仿射,微积分,的就好,其实我自己也搜索过很多资料。
就是没找到好的资源,可能自己的基础太差,没法理解真正的几何意义。
Start 开始
今天写的栅格器,运行效果
发现对底层还是很多不了解的,要先暂停,看一下理论知识,总结差不多,再继续写
一边学习OpenGL,一边总结写到软渲染器中。
但是性能不用考虑,因为我只渲染一帧。
法线渲染一帧都挺卡的(-_-!),特别是片段生成多的时候,更加卡。。。
2019.07.16 优化了Bitmap的像素设置方式。
才发现底层有更好的接口来画画布。
下面是设置了一个Timer.Interval = 15ms、10ms、1ms都试过。FPS都不变。
然后我将,Draw的代码屏蔽掉,结果FPS还是一样。
估计的Timer内部的Tick事件限制频率了。
2019.07.17 添加了Blend
片段多了,性能还是上不去。-_-!
上面的GIF是旧的,因为之前没发现BitmapData的颜色指针的数据中,是BGRA而不是RGBA。
所以混合效果有些不太一样,这个问题就修复。
2019.07.17添加了透视相机、混合优化
投影效果:
渲染管线中的将几何3D点集统一经过一系列的变换流程,最终生成2D窗口坐标。
以下是变换顺序:
下面就是最终在窗口的坐标(Window Coordinates)
后面重构成:ShadingMode了
类似的,后面还会再添加一个:WifreframeType,可以是Point、Line,两种。
这个是对光栅的像素做剔除用的,就设置一个矩形(x,y,width,height)
我看很多资料教程都是将这个裁剪矩形片段剔除都是再片段着色器后处理的,不知道为何这样做。
而我的软渲染器里,是放在片段着色器前,即:光栅插值出片段的x,y坐标时,就会去判断裁剪矩形剔除。
一般AlphaTest是在Scissor会后,对着色之后的片段alpha值进行AlphaTestComp的比较模式来确定剔除关系。
在我的软渲染器里,我是放在片段着色器后,再对alpha测试,因为片段着色器会有可能对片段的alpha值做修改。
添加了个Cube,先在还没加深度测试,所以在下面的GIF可以看到我在切换Cull为Off时,会有一些背面的信息画在了正面上
简单写了个类似Unity的Camera属性的封装类。
在Camera属性中可以调整对应的属性。
属性分类:
封装了一个简单易用的GameObject,没去写Component系统。
因为验证渲染功能,后续有空可完善一下
简单封装了Mesh对象,有点类似Unity的Mesh也是包含一下内容
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/(F−N)
b = N F / ( N − F ) b = NF / (N - F) b=NF/(N−F)
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 depth−a=b/Z
( d e p t h − a ) / b = 1 / Z (depth - a) / b = 1 / Z (depth−a)/b=1/Z
b / ( d e p t h − a ) = Z b / (depth - a) = Z b/(depth−a)=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/(depth−a)=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/(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就是要输入缓存的值。
这个设计与Unity差不多。
2019.07.19 简单添加了方向光光照,我们下面还显示法线的调试功能,便于调试光照
下面是添加了半兰伯特光照LightDotNormal*0.5+0.5
现在我是直接添加到逐像素处理的硬编码
后续我会再添加一个可编程的VertexShader与FragmentShader
然后我会将VertexData数据都封装到Mesh类中,每帧都计算好对应Shader需要用的数据并上传到Renderer中(对于OpenGL或Dx或Vulkan就是上传到Graphicis hardware图形硬件中,即:显卡),以供后面的Shader阶段使用
类似Unity中的ShaderLab里的Offset [factor, 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了解:
(其他的功能正在添加中,因为对OpenGL不熟悉,在一边看书,一边写程序)
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);
}
}
}
再来详细说明。
下面是顶点着色器的示例。
[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] public Matrix4x4 MVP;
[Uniform] public Matrix4x4 M;
使用[UniformAttribute]标记的字段,都是该shader的Uniform数据(我这儿的Uniform也是可以在shader运行中更改的,不想OpenGL之类的,你对Uniform设置了,可能都会编译不通过)。Uniform数据通常是外部传进来的多个shader之间通用的数据,这个是针对但个Shader类的数据,我们也封装了一个是多个Shader对象都可以共享访问的数据有点类似Uniform block:是构造函数传进来的BasicShaderData data数据对象。
这个数据对象在外部可以使用Renderer.ShaderData来设置,也可以重置成你想要的类。
例如,我们的相机位置,灯光位置,我们都可以放这里,这样多个shader之间都可以拿到这些全局的共享数据。
[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] [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]的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个寄存器的概念)
可以看到我们之类的Shader中的Color、Texcoord都没加上location的定义,所以默认就是0的location值。
SV_Target的location控制片段着色器输出到那个RT对象(后面再实现MRT)。其他的location是用于控制类似寄存器的区别的概念。
片段着色器的我就不多介绍了,大部分与定点着色器相同,注意一下几点:
在上面的代码下,运行情况是:(我使用的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通道,以下是运行效果:
Pass的实现还是需要写比较多的代码,虽然也是可以实现的
后续还有很多其他功能,留着以后有空再写了。
2019.07.27,但是效果失败了,如下描述
我按照了《3D游戏与计算机图形学的中数学方法》的第66~68页的内容处理了。
也参考了一下的连接:
透视校正插值(Perspective-Correct Interpolation)
透视下的插值(Perspective-Correct Interpolation)
透视校正插值
3D图形学学习总结(十)—纹理映射透视矫正 - 这个博客直接推导公式,可以参考里头的原理
只看Perspective Correct Texture Mapping部分 - 未尝试
3D 图形光栅化的透视校正问题 - 未看完
为了测试,使用了修改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# 实现精简版的栅格化渲染器 - 代码很精简,修复了投影校正的问题
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);
这里就没去封装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
}
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的算法比较简单
主要代码是:
[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);
}
}
其他都比它简单,使用图解具象化一下:value与mirror-value值与的变化关系
先是UV坐标:
然后是value与mirror-value的关系图解:
从图中,我们可以看到一个规律:
无论正、负数,偶、奇数,可以发现他们都在0f~1f的值,所以一开始就需要先折叠一下:
m i r r o r − v a l u e = v a l u e % 1 f ; mirror-value=value\%1f; mirror−value=value%1f;
但是正负之间有所区别
正数的处理:
负数的处理:
在上面完整代码中也可以看到Mirror(X/Y)的处理就是这样的。
最后来个纹理、顶点颜色的显示;另一个是开了叠加混合(开了混合后发现边框的片段有写重复,后续优化)
var c = new ColorNormalized(inUV.x, inUV.y, 1, 1);
outColor = tex2D(sampler, mainTex, inUV) + c; // * c;
对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;
}
}
}
看看加入了法线之后的光照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;
}
}
}
法线添加了光照之后,法线栅格化的像素有问题,不知道是否有冗余的片段的问题。
2019.08.01
这次增加法线的方向的渐变,源点为蓝色,方向为白色,如下GIF:
2019.08.01
因为有个法线错乱的问题,困扰很久了。不得不把TBN显示出来调试用。
2019.08.03
控制方式我仿Unity的方式,不过有时会有万向锁的问题。
下面值演示其中一种:鼠标右键+w,s,a,d键的方式
2019.08.03
这里我的图元裁剪是整个图元裁剪的方式
在上面的镜头控制中,可以看到,图元的其中一个顶点只要超出了相机视椎体范围,都会被裁剪掉,改而显示成线框内容。
2019.08.03
*.m文件是我在Unity中使用脚本将网格类Mesh.IsReadable,读取出来的模型。
如果Mesh.IsReadable==false的将读取不了。
下面是加载了Unity中的Sphere球体。我将它表面显示出法线来。
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就受不了了。-_-!
2019.08.04
不过代码是临时结构,后面再优化
我这个抗锯齿的思路是:(与市面上的算法可能不一致,我这个只是该是思路,具体算法,不是这样的,可以参考我下面列出的连接,这儿我就再列出来吧:基于图片的抗锯齿方法(一))
说说查找边缘算法的思路(与描边思路一样):
每种算法各有优缺点:
所以,在根据具体需求来选择不同的描边处理,或是组合处理(像素+深度+法线)。
抗锯齿、描边的算法我接触不多,应该还有很多其他的方式。
其实我上面的算法的效果不是很好。
MSAA抗锯齿可以看看这篇:基于图片的抗锯齿方法(一)
2019.08.04
均值模糊就比较简单了
就是取临近像素的值混合在一起,与上面抗锯齿的模糊算法一样,看看效果:
2019.08.06
帧缓存主要包括:
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));
}
}
}
2019.08.07,今天去看胃病,没时间写,花了一点时间,就将之前铺垫已久的Stencil一下就加进来了
描边可参考:https://blog.csdn.net/linjf520/article/details/95064552#t10
效果图如下:(右边的图是我对SV_Target(1)输出了法线的贴图,然后我显示出来了,黄色描边的法线颜色SV_Target(1)我没赋值,所以默认是(0,0,0,1)的颜色:黑色)
2019.08.07 今天去医院看老胃病了,不过还好只是花了一点时间,就将之前铺垫已久的Stencil功能加进来了
先准备一下mask图,我使用GIMP随便画了一张
然后调整一下所有需要遮罩的shader,运行效果
GIF录制了很多次都时后面就会花屏,不知道什么原因
我们准备了另一个图,也是使用GIMP来随表画的
运行效果,也是录制到后面就花屏,应该是软件的BUG
为了修复法线问题,自己搞了个excel来验算,输入各种参数,即可立马得到结果
文件:验算矩阵Exxcel
如下图:
因为计划有所变动,暂时停止了渲染器开发。
但是留下了一些比较重要需要实现的内容:
待后面有空,我会将这两块功能加入。
(本想继续写下去的,但发现时间有限,还是等后续有空,一边学习,一边补上其他的功能。)
自己写的这个软渲染功能,都是一边查资料恶补图形知识这块,一边写,很多代码、结构写法,为了可读性,没去优化,所以优化空间还有很大。
写的这些都是显卡很基础的处理,而且显卡发展了这么多年了。
都是经历过各种管线优化,算法优化。
总得来说GPU 的渲染管线给CPU分担了N多计算。
特别是GPU的各种纵向管线、横向并行的计算单元资源,都将优化最大化了。
最后,最大感悟:数学,真的可以改变世界。可惜我没学好。T^T, QAQ。
3DSoftRenderer