聊聊引擎底层如何编译Shader脚本

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxw167/article/details/85008461

由于现在的引擎都比较完善,开发者无需关心引擎底层的技术,只负责写逻辑即可,比如Unity引擎和UE4引擎,它们的出现确实为开发者提供了便利,但是对开发者来说,笔者认为并不是好事情,它导致了大部分开发者只能做些没有任何科技含量的东西,请允许我这么说,因为我们只是在如何用好引擎方面大动脑筋,更要命的是我们使用的引擎都是国外开发的,人家是做底层的技术,换句话说,核心技术掌握在人家手里,作为程序员来说应该感到悲哀。因为现阶段我也在写自己的3D引擎,所以就花了一些时间研究了引擎底层的技术分享给大家,过程是艰苦的,但是我们还是应该坚持自己的追求。现将学习到的技术分享给读者,进而解开Shader在引擎底层的处理方式。这样读者在使用成熟引擎编写Shader脚本时,至少知道它们底层是如何处理的,中国的引擎技术振兴就靠大家了,下面我们进入主题。
我们编写Shader渲染模型材质和后处理渲染,只需要把编写好的Shader放到引擎指定的目录下面就可以了,后面的处理就教给引擎底层了。引擎底层它是如何处理的呢?先看看 引擎实时渲染架构图:
在这里插入图片描述
我们本篇博客主要介绍Shader在引擎中的加载,编译,申请缓冲区,利用DX提供的接口实现我们的功能。

  • Shader脚本加载

    首先我们要知道Shader的格式以及文件存放路径,就是我们常说的Shader脚本加载,在引擎中可以通过遍历文件夹中的文件即可获取到Shader文件,加载Shader脚本代码跟加载文件的处理方式类似,下面直接把实现函数给读者展示一下:
    在这里插入图片描述
    上面代码,我们将Shader脚本全部加载进来,Shader的加载可以通过遍历文件夹获取到。

  • Shader编译

    下面看一下Shader的编译,在引擎中我们使用了Shader缓冲技术,就是说如果Shader已经被加载了,再使用时直接从缓存中加载即可,否则就重新创建,代码如下所示:

  const std::string cacheFileName = DirectoryUtil::GetFileNameWithoutExtension(sourceFilePath) + ".hlsl.bin";
  const std::string cacheFilePath = s_shaderCacheDirectory + "\\" + cacheFileName;
  const bool bUseCachedShaders = DirectoryUtil::FileExists(cacheFilePath) && !IsCacheDirty(sourceFilePath, cacheFilePath);
if (bUseCachedShaders)
  {
   blobs.of[type] = CompileFromCachedBinary(cacheFilePath);
  }
  else
  {
   blobs.of[type] = CompileFromSource(sourceFilePath, type);
   CacheShaderBinary(cacheFilePath, blobs.of[type]);
  }
  CreateShader(pDevice, type, blobs.of[type]->GetBufferPointer(), blobs.of[type]->GetBufferSize()); 

在上述代码中首先看一下缓存中是否存在,存在就直接编译,否则如果不存在就重新加载编译,然后创建Shader,注意这里只是把Shader进行了编译,先看看Shader是否有错误,使用过Unity引擎中Shader的开发者应该遇到过Shader书写错误,会报错的情况,这就是先被引擎编译的结果,它还没被应用到材质上或后处理上。我们先看看看从缓存中编译函数的实现。
在这里插入图片描述
在函数中,我们返回的参数选择了ID3D10Blob,它表示什么含义呢?查看微软提供的文档:
在这里插入图片描述它可以存储顶点,材质信息,在函数中调用了函数D3DCreateBlob,该函数用于创建一个缓存Buffer。其他相信大家都能看得明白就不一一解释了。
再看上述函数中的另一个函数接口:CompileFromSource,这函数实现也比较简单:
在这里插入图片描述
再看看CreateShader函数的实现,我们把Shader编译后,要把Shader中顶点,片段,几何分离一下,至少让GPU知道如何执行。实现代码如下所示:
在这里插入图片描述
调用的也是DX提供的接口,讲到这里还没有完结,我们加载Shader的目的是让它帮我们渲染,我们要获取到已经编译好的Shader,DX为我们提供了一个反射接口:ID3D11ShaderReflection,用于访问Shader信息。为此我们定义了一个联合体,如下所示:
在这里插入图片描述
另外还需要一个函数帮我们处理,D3D11Reflect 获取指向反射接口的指针。对应的代码如下所示:
在这里插入图片描述
这样我们就建立了与Shader的关系,换句话说我们就可以访问编译的信息了,但是这个还没有结束,我们现在还不知道加载的信息都是啥?需要对Shader已经编译的信息进行分析处理。

  • 设置缓存区
    我们编写的Shader会定义一些变量,纹理,数据或者定义常量等等,这些我们需要在引擎中对它们进行处理,这样你才可以通过可视化界面把纹理和数据以及矩阵传递进去。下面我们看看引擎怎么对它们进行处理的。 我们要将变量放到缓冲区中,这个缓存区是CPU和GPU共同拥有的,我们要为Shader中的需要存放的信息申请缓冲区,我们的实现函数如下所示:
    在这里插入图片描述
    最后一步要创建CPU和GPU缓冲区,CPU是为了存储要传送给GPU的数据,而GPU自身也有显存的,用于存放Shader定义的变量。 下面先创建CPU开辟内存代码如下所示:
    在这里插入图片描述
    就是把我们定义的变量放到对应的缓冲区中,下面再看看GPU的缓冲区,代码如下所示:
    在这里插入图片描述
    在这里大家可能有个疑问,在Unity中的DrawCall讲解时,CPU会开辟一块内存,用于连通CPU与GPU,那我们这个为什么开辟两块内存?二者并不矛盾,我们在CPU中大家可以看一下我们遍历时使用的for循环,二者时一致的,都是从同一块内存中共享数据。可以参考网上的代码,地址如下所示:
    http://gamedev.stackexchange.com/a/62395/39920

  • 总结
    我们对于Shader的处理,首先是将它们加载到内存中,然后编译Shader,对Shader中的信息进行分类,找到顶点着色器,片段着色器和几何着色器,还有把Shader脚本定义的数据开辟一块内存存放。Shader在引擎中的处理就介绍完了,后面的渲染就比较顺利了,后面继续引擎底层分析。

猜你喜欢

转载自blog.csdn.net/jxw167/article/details/85008461