第1页 |
定义常量缓冲区。使用渲染管道核心库。支持动态批处理和GPU实例化。这是包含Unity可编写脚本的渲染的教程系列的第二部分管道。它是关于使用HLSL创建着色器并有效地渲染多个着色器通过在单个绘图调用中对它们进行批处理来对象。本教程使用Unity 2018.3.0f2。256个球,一个平局调用。
第2页 |
1个自定义不亮的着色器虽然我们已经使用默认的unlit着色器来测试我们的管道,但需要完整一个非常重要的自定义管道的优点需要创建自定义着色器与它合作。所以我们将创建一个自己的着色器,取代Unity的默认值不亮的着色器。
第3页 |
1.1 创建着色器可以通过Assets / Create / Shader中的一个选项创建着色器资源菜单。Unlit Shader是最合适的,但我们将重新开始从创建的着色器文件中删除所有默认代码。将资产命名为Unlit。不亮的着色器资产。着色器文件的基础知识在渲染2,着色器基础知识中进行了解释。如果您不熟悉编写着色器,请仔细阅读,以便了解基础知识。该获得有效着色器的最小值是使用Properties块定义Shader块加上一个带有Pass块的SubShader块。Unity会将其转换为默认值白色未点亮着色器。在之后的Shader关键字来将在使用的字符串材质的着色器下拉菜单。我们将使用My Pipeline / Unlit。着色器“我的管道/熄灭” {属性{}SubShader {通过{}}}调整Unlit Opaque材质,使其使用我们的新着色器,如果将其变为白色它还没有。带有自定义着色器的不透明不透明材质。1.2 HLSL
第4页 |
要编写我们自己的着色器,我们必须在其Pass块中放置一个程序。统一支持GLSL或HLSL程序。虽然GLSL用于默认着色器和同样在渲染2,Shader Fundamentals, Unity的新渲染管道着色器使用HLSL,所以我们也将它用于我们的管道。这意味着我们必须把所有我们在HLSLPROGRAM和ENDHLSL语句之间的代码。通过{HLSLPROGRAMENDHLSL}GLSL和HLSL计划有什么区别?在实践中,Unity对两者使用几乎相同的语法并负责转换每个构建目标的适当着色器代码。最大的区别是GLSL程序默认包含一些代码。HLSL程序不会隐式执行任何操作,要求我们明确包含我们需要的任何东西。那很好,因为旧的GLSL包含文件由旧的和过时的代码压缩。我们将依赖更新的HLSL包括文件。Unity着色器至少需要顶点程序和片段程序函数,每个都使用pragma编译器指令定义。我们将使用UnlitPassVertex为另一个是顶点函数和UnlitPassFragment。但是我们不会把代码放进去这些函数直接在着色器文件中。相反,我们将把HLSL代码放入单独的包含文件,我们也将其命名为Unlit,但使用hlsl扩展名。把它放进去与Unlit.shader相同的文件夹,然后将其包含在HLSL程序之后编译指令。HLSLPROGRAM#pragma vertex UnlitPassVertex#pragma片段UnlitPassFragment#include“Unlit.hlsl”ENDHLSL不幸的是,Unity没有一个方便的菜单项来创建HLSL包括文件资产。你必须自己创建它,例如通过复制Unlit.shader文件,将其文件扩展名更改为hlsl并删除着色器代码从中。
第5页 |
Unlit HLSL包含文件资产。在include文件中,以include guard开头,以防止重复代码如果文件被包含多一次。虽然这应该永远不会发生,但这很好练习总是为每个包含文件执行此操作。#ifndef MYRP_UNLIT_INCLUDED#define MYRP_UNLIT_INCLUDED#endif // MYRP_UNLIT_INCLUDED至少,我们必须知道顶点程序中的顶点位置,它具有输出均匀的剪辑空间位置。所以我们将定义输入和输出顶点程序的结构,都有一个float4位置。#ifndef MYRP_UNLIT_INCLUDED#define MYRP_UNLIT_INCLUDEDstruct VertexInput {float4 pos:POSITION ;};struct VertexOutput {float4 clipPos:SV_POSITION ;};#endif // MYRP_UNLIT_INCLUDED接下来,我们将定义顶点程序函数UnlitPassVertex。现在,我们将直接使用对象空间顶点位置作为剪辑空间位置。这是不正确的,但是是获得编译着色器的最快方法。我们将添加正确的空间转换后来。struct VertexOutput {float4 clipPos:SV_POSITION ;};VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;output.clipPos = input.pos;返回输出;}#endif // MYRP_UNLIT_INCLUDED
第6页 |
我们现在保留默认的白色,所以我们的片段程序功能可以只需将1作为float4返回。它接收内插顶点输出作为其输入,所以将其添加为参数,即使我们尚未使用它。VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;output.clipPos = input.pos;返回输出;}个float4 UnlitPassFragment(VertexOutput 输入):SV_TARGET {返回1 ;}#endif // MYRP_UNLIT_INCLUDED我们应该使用half还是float?大多数移动GPU支持两种精度类型,一半更高效。所以,如果你是针对手机进行优化,尽可能多地使用一半是有意义的。规则是仅使用浮点数来表示位置和纹理坐标,对于其他一切使用一半,只要结果可以接受。当不针对移动平台时,精度不是问题,因为GPU始终如此使用浮动,即使我们写了一半。我会在本教程系列中一直使用float。还有固定类型,但它只有你真正支持旧硬件不会针对现代应用程序。它通常相当于一半。1.3 转换矩阵此时我们有一个编译着色器,虽然它不会产生明显的结果然而。下一步是将顶点位置转换为正确的空间。如果我们有一个模型 - 视图 - 投影矩阵然后我们可以直接从对象空间转换为剪辑空间,但Unity不会为我们创建这样的矩阵。它确实制作了模型矩阵可用,我们可以用它从对象空间转换为世界空间。统一我们希望我们的着色器有一个float4x4 unity_ObjectToWorld变量来存储矩阵。当我们使用HLSL时,我们必须自己定义该变量。然后用它来转换顶点函数中的世界空间并将其用于输出。
第7页 |
float4x4 unity_ObjectToWorld;struct VertexInput {float4 pos:POSITION ;};struct VertexOutput {float4 clipPos:SV_POSITION ;};VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;float4 worldPos =mul(unity_ObjectToWorld,input.pos);output.clipPos = worldPos;返回输出;}接下来,我们需要将世界空间转换为剪辑空间。这是以观点完成的 -投影矩阵,Unity通过float4x4 unity_MatrixVP变量提供。添加它然后完成转换。float4x4 unity_MatrixVP;float4x4 unity_ObjectToWorld;...VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;float4 worldPos =mul(unity_ObjectToWorld,input.pos);output.clipPos = mul(unity_MatrixVP,worldPos);返回输出;}我更改了代码,但它仍然无法正常工作?编辑包含文件时,Unity并不总是响应更改而无法刷新着色器。发生这种情况时,请再次保存文件,如有必要,请再次尝试您可以稍后撤消的小改动。我们的着色器现在可以正常工作 使用未点亮材料的所有对象都是再次出现的可见,全白。但是我们的转换效率并不高,因为它是用4D位置矢量执行全矩阵乘法。第四个该职位的组成部分始终为1.通过明确表示我们使其成为可能用于编译器优化计算。float4 worldPos =MUL(unity_ObjectToWorld,个float4(input.pos。 XYZ,1.0));
第8页 |
优化是否有意义?这是Unity自身的优化,非常热衷于升级所有着色器每个人都使用它。这是mad和add指令之间的区别。是否这是一个明显的差异取决于平台。无论如何,它只能更快,从不慢。这是没有的空间转换生成的D3D11代码优化:0 :mul r0。xyzw,v0。yyyy,cb1 [ 1 ]。XYZW1 :疯狂r0。xyzw,cb1 [ 0 ]。xyzw,v0。xxxx,r0。XYZW2 :疯狂r0。xyzw,cb1 [ 2 ]。xyzw,v0。zzzz,r0。XYZW3 :狂r0。xyzw,cb1 [ 3 ]。xyzw,v0。wwww,r0。XYZW4 :mul r1。xyzw,r0。yyyy,cb0 [ 1 ]。XYZW5 :狂r1。xyzw,cb0 [ 0 ]。xyzw,r0。xxxx,r1。XYZW6 :狂r1。xyzw,cb0 [ 2 ]。xyzw,r0。zzzz,r1。XYZW7 :疯了o0。xyzw,cb0 [ 3 ]。xyzw,r0。wwww,r1。XYZW以下是优化转换:0 :mul r0。xyzw,v0。yyyy,cb1 [ 1 ]。XYZW1 :疯狂r0。xyzw,cb1 [ 0 ]。xyzw,v0。xxxx,r0。XYZW2 :疯狂r0。xyzw,cb1 [ 2 ]。xyzw,v0。zzzz,r0。XYZW3 :添加r0。xyzw,r0。xyzw,cb1 [ 3 ]。XYZW4 :mul r1。xyzw,r0。yyyy,cb0 [ 1 ]。XYZW5 :狂r1。xyzw,cb0 [ 0 ]。xyzw,r0。xxxx,r1。XYZW6 :狂r1。xyzw,cb0 [ 2 ]。xyzw,r0。zzzz,r1。XYZW7 :疯了o0。xyzw,cb0 [ 3 ]。xyzw,r0。wwww,r1。XYZW
第9页 |
1.4 常数缓冲区Unity没有为我们提供模型 - 视图 - 投影矩阵,因为这样一来可以避免M和VP矩阵的矩阵乘法。除此之外,副总裁矩阵可以重复用于在同一相机中绘制的所有内容帧。Unity的着色器利用了这一事实并将矩阵置于不同的位置恒定缓冲区。虽然我们将它们定义为变量,但它们的数据保持不变在绘制单个形状期间,通常比这更长。VP矩阵得到放入每帧缓冲区,而M矩阵放入每个绘制缓冲区。虽然并不是严格要求将着色器变量放在常量缓冲区中,但这样做使得可以更有效地更改同一缓冲区中的所有数据。在至少,当图形API支持它时就是这种情况。OpenGL没有。为了尽可能高效,我们还将使用常量缓冲区。Unity提出了VP矩阵在UnityPerFrame缓冲器,并在M个矩阵UnityPerDraw缓冲器。有更多的数据放在这些缓冲区中,但我们还不需要它,所以没有必要包括它。除了cbuffer之外,常量缓冲区定义为结构关键字和变量仍然可以像以前一样访问。cbuffer UnityPerFrame {float4x4 unity_MatrixVP;};cbuffer UnityPerDraw {float4x4 unity_ObjectToWorld;}1.5 核心库由于常量缓冲区不会使所有平台受益,因此Unity的着色器依赖于宏只在需要时使用它们。带有name参数的CBUFFER_START宏是而不是直接编写cbuffer和随附的CBUFFER_END宏替换缓冲区的末尾。我们也可以使用这种方法。CBUFFER_START (UnityPerFrame)float4x4 unity_MatrixVP;CBUFFER_ENDCBUFFER_START (UnityPerDraw)float4x4 unity_ObjectToWorld;CBUFFER_END
第10页 |
这会导致编译器错误,因为未定义这两个宏。宁比较何时适合使用常量缓冲区并定义宏我们自己,我们将利用Unity的核心库来渲染管道。它可以添加通过包管理器窗口访问我们的项目。切换到所有包列表和启用在Advanced下显示预览包,然后选择Render-pipelines.core,并安装它。我正在使用版本4.6.0-preview,这是在Unity中运行的最高版本2018.3。渲染管道核心库已安装。现在我们可以包含公共库功能,我们可以通过它访问包/ com.unity.render-pipelines.core / ShaderLibrary / Common.hlsl。它定义了多个有用的函数和宏,以及常量缓冲区宏,所以在使用之前包括它。#include“Packages / com.unity.render-pipelines.core / ShaderLibrary / Common.hlsl”CBUFFER_START (UnityPerFrame)float4x4 unity_MatrixVP;CBUFFER_END这些宏如何正常工作?您可以通过打开核心库包中的Common.hlsl文件来查看。它结束了包括API子文件夹中的API特定包含文件,该文件定义了宏。
第11页 |
1.6 编译目标等级我们的着色器再次工作,至少对于大多数平台而言。包括图书馆,我们的shader无法为OpenGL ES 2编译。这是因为Unity默认使用OpenGL ES 2的着色器编译器,不适用于核心库。我们可以解决通过在我们的着色器中添加#pragma prefer_hlslcc gles,这就是Unity所做的它在Lightweight渲染管道中的着色器。但是,而不是我们这样做根本不支持OpenGL ES 2,因为它只适用于旧版本移动设备。我们通过使用#pragma target指令来定位着色器级别3.5而不是默认级别,即2.5。#pragma target 3.5#pragma vertex UnlitPassVertex#pragma片段UnlitPassFragment1.7 文件夹结构请注意,核心库的所有HLSL包含文件都位于ShaderLibrary文件夹中。我们也这样做,所以将Unlit.hlsl放在My Pipeline中的新ShaderLibrary文件夹中。将着色器也放在单独的Shader文件夹中。我的Pipeline文件夹结构。为了保持我们的着色器完好无损,同时仍然依赖于相对包含路径,我们必须这样做将我们的include语句从Unlit.hlsl更改为../ShaderLibrary/Unlit.hlsl。#include“../ShaderLibrary/Unlit.hlsl”
第12页 |
2 动态批处理现在我们有了一个最小的自定义着色器,我们可以用它来进一步研究如何我们的管道渲染东西。一个很大的问题是它的效率如何。我们要测试一下通过用一堆使用我们未点亮的材料的球体填充场景。您可以使用数千,但几十也可以传达信息。他们可以拥有不同的变换,但保持他们的鳞片均匀,意味着每个尺度的X,Y和Z分量始终相等。一束白色球形。在研究如何通过帧调试器绘制场景时,您会注意到这一点每个球体都需要自己独立的绘制调用。这不是很有效,因为每个绘制调用引入了CPU和GPU需要通信的开销。理想的情况下,多个球体与一次通话一起绘制。虽然这是可能的,但它目前没有发生。框架调试器给你一个关于它的提示选择其中一个绘制调用。没有动态批处理。2.1 启用批处理框架调试器告诉我们不使用动态批处理,因为它也是因为深度排序干扰了它。如果您检查播放器设置,然后你会看到动态批处理选项确实被禁用了。然而,启用它没有任何效果。这是因为播放器设置适用于Unity的默认设置管道,而不是我们的定制。
第13页 |
要为我们的管道启用动态批处理,我们必须指出它是允许的在MyPipeline .Render中绘图时。绘图设置包含我们的标志字段必须设置为DrawRendererFlags .EnableDynamicBatching。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“SRPDefaultUnlit”));drawSettings.flags = DrawRendererFlags .EnableDynamicBatching;drawSettings.sorting.flags = SortFlags .CommonOpaque;在那次改变后,我们仍然没有动态批处理,但原因已经改变。动态批处理意味着Unity之前将对象合并在一个网格中他们被吸引了。这需要每帧的CPU时间并保持检查它仅限于小网格。要批处理的顶点太多。球体网格太大,但立方体很小并且可以工作。所以调整所有对象改为使用立方体网格。您可以全部选择它们并将它们的网格过滤器调整为一个走。在单个动态批次中绘制的立方体。2.2 颜色
第14页 |
动态批处理适用于使用相同材质绘制的小网格。但是当涉及多种材料时,事情变得更加复杂。为了显示这样,我们就可以改变未点亮着色器的颜色。添加颜色属性的属性块,叫做_Color,色彩搭配作为其标签,采用白色作为默认。属性{_Color( “ 颜色”,颜色)=(1,1,1,1)}材料调整颜色。现在我们可以调整材质的颜色,但它不会影响绘制的内容。将float4 _Color变量添加到我们的包含文件中并返回而不是修复UnlitPassFragment中的值。颜色是根据材料定义的,因此可以放入恒定缓冲区,只需要在切换材料时更改。我们会说出来的缓冲UnityPerMaterial,就像Unity一样。CBUFFER_START (UnityPerDraw)float4x4 unity_ObjectToWorld;CBUFFER_ENDCBUFFER_START (UnityPerMaterial)float4 _Color;CBUFFER_ENDstruct VertexInput {float4 pos:POSITION ;};...个float4 UnlitPassFragment(VertexOutput 输入):SV_TARGET {return _Color;}复制我们的材料,并设置两者使用不同的颜色,所以我们可以区分他们。选择一些对象并让它们使用新材料,最后得到一个混合。
第15页 |
两种材料,四批。动态批处理仍然会发生,但我们最终会有多批次。会有每种材料至少一批,因为每种材料需要不同的每种材料数据。但通常会有更多的批次,因为Unity更喜欢在空间上对对象进行分组减少透支。由于材料不同,没有批处理。2.3可选配批动态批处理可能是一个好处,但它也可能最终没有做多少差异,甚至放慢速度。如果你的场景不包含很多小的共享相同材质的网格,禁用动态可能有意义批处理因此Unity不必弄清楚每个框架是否使用它。所以我们将添加一个选项,以便为我们的管道启用动态批处理。我们不能依赖播放器设置。相反,我们将一个切换配置选项添加到MyPipelineAsset,所以我们可以通过编辑器中的管道资产来配置它。[ SerializeField ]bool dynamicBatching;
第16页 |
启用动态批处理。当MyPipeline被创建的实例,我们要告诉它是否使用动态批量与否。我们将在调用它时将此信息作为参数提供构造函数。protected override IRenderPipeline InternalCreatePipeline(){返回新的MyPipeline(dynamicBatching);}为了使这项工作,我们不能再依赖MyPipeline的默认构造函数。给它一个公共构造函数方法,使用布尔参数来控制动态配料。我们将在构造函数中设置一次绘制的标志并跟踪它们在一个领域。DrawRendererFlags drawFlags;public MyPipeline(bool dynamicBatching){if (dynamicBatching){drawFlags = DrawRendererFlags .EnableDynamicBatching;}}将标志复制到渲染中的绘制设置。drawSettings.flags = drawFlags;请注意,当我们在编辑器中切换资产的动态批处理选项时,Unity的批处理行为立即发生变化。每次我们调整资产一个新的管道实例已创建。
第17页 |
3 GPU实例化动态批处理不是我们减少绘制数量的唯一方法每帧呼叫。另一种方法是使用GPU实例化。在实例化的情况下,CPU告诉GPU不止一次地绘制特定的网格材质组合通过一个平局电话。这使得可以对使用相同对象的对象进行分组网格和材料,而不必构造新的网格。这也消除了限制网格尺寸。3.1 可选实例化默认情况下启用GPU实例化,但我们通过自定义绘制覆盖了它标志。让GPU实例化也是可选的,这样可以很容易地比较它有和没有它的结果。将另一个切换添加到MyPipelineAsset并将其传递给构造函数调用。[ SerializeField ]布尔实例;protected override IRenderPipeline InternalCreatePipeline(){返回新的MyPipeline(dynamicBatching,instancing);}在MyPipeline构造函数方法中,还要在执行此操作后设置实例化标志用于动态批处理。在这种情况下,flags值为DrawRendererFlags .EnableInstancing我们布尔 - 或者它进入标志,所以动态批处理和实例化都可以同时启用。当它们都被启用时,Unity更喜欢实例化配料。public MyPipeline(bool dynamicBatching,bool instancing){if (dynamicBatching){drawFlags = DrawRendererFlags .EnableDynamicBatching;}if (instancing){drawFlags | = DrawRendererFlags.EnableInstancing;}}启用实例化,禁用动态批处理。
第18页 |
3.2 材料支持为我们的管道启用GPU实例并不意味着对象是自动实例化。它必须由他们使用的材料支持。因为并不总是需要实例化,所以它是可选的,这需要两个着色器变体:一个支持实例化,另一个不支持实例化。我们可以创造所有通过将#pragma multi_compile_instancing指令添加到我们的着色器。在我们的例子中,它产生两个着色器变体,一个有一个,一个没有已定义 INSTANCING_ON 关键字。#pragma target 3.5#pragma multi_compile_instancing#pragma vertex UnlitPassVertex#pragma片段UnlitPassFragment该更改还会为我们的材料显示新的材料配置选项:启用GPU实例化。启用了实例化的材料。3.3 着色器支持启用实例化时,会告诉GPU多次绘制相同的网格具有相同的常数数据。但M矩阵是该数据的一部分。这意味着我们最终以完全相同的方式多次渲染相同的网格。要得到围绕这个问题,必须放置一个包含所有对象的M矩阵的数组在一个恒定的缓冲区。每个实例都使用自己的索引绘制,可以使用它从数组中检索正确的M矩阵。
第19页 |
我们现在必须在不实例化时使用unity_ObjectToWorld,或者使用矩阵数组当我们实例化的时候。为了使两种情况下UnlitPassVertex中的代码保持相同,我们将为矩阵定义一个宏,特别是UNITY_MATRIX_M。我们使用那个宏name,因为核心库有一个包含文件,用于定义要支持的宏为我们实例化,它还重新定义了UNITY_MATRIX_M,以便在使用矩阵数组时需要。CBUFFER_START (UnityPerDraw)float4x4 unity_ObjectToWorld;CBUFFER_END#define UNITY_MATRIX_M unity_ObjectToWorld...VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;float4 worldPos =MUL( UNITY_MATRIX_M,个float4(input.pos。 XYZ,1.0));output.clipPos = mul(unity_MatrixVP,worldPos);返回输出;}包含文件是UnityInstancing.hlsl,因为它可能会重新定义UNITY_MATRIX_M我们必须在自己定义宏之后包含它。#define UNITY_MATRIX_M unity_ObjectToWorld#include“Packages / com.unity.render-pipelines.core / ShaderLibrary / UnityInstancing.hlsl”使用实例化时,会添加当前正在绘制的对象的索引它是GPU的顶点数据。该UNITY_MATRIX_M依赖于指数,所以我们要将其添加到VertexInput结构中。我们可以使用UNITY_VERTEX_INPUT_INSTANCE_ID那个宏。struct VertexInput {float4 pos:POSITION ;UNITY_VERTEX_INPUT_INSTANCE_ID};最后,我们必须在使用前赚了指数提供UNITY_MATRIX_M在UnlitPassVertex ,通过UNITY_SETUP_INSTANCE_ID宏,提供输入作为论点。
第20页 |
VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;UNITY_SETUP_INSTANCE_ID (输入);float4 worldPos =MUL( UNITY_MATRIX_M,个float4(input.pos。 XYZ,1.0));output.clipPos = mul(unity_MatrixVP,worldPos);返回输出;}我们的立方体现在得到实例化。就像动态批处理一样,我们最终得到了多批次,因为我们使用不同的材料。确保所有材料使用GPU实例启用。四个实例绘制调用。除了对象到世界的矩阵之外,默认情况下还会放入世界到对象的矩阵实例化缓冲区也是如此。那些是需要的M矩阵的逆矩阵对于使用非均匀尺度的法向量。但我们只使用制服比例,所以我们不需要那些额外的矩阵。我们可以通知Unity,通过将#pragma instancing_options assumeuniformscaling指令添加到我们的着色器中。#pragma multi_compile_instancing#pragma instancing_options assumeuniformscaling如果您确实需要支持非均匀缩放,那么您将必须使用着色器没有启用此选项。3.4 多种颜色如果我们想在场景中包含更多颜色,我们需要做更多材料,这意味着我们最终批量生产。但是如果矩阵可以放在数组中,应该可以对颜色做同样的事情。然后我们可以结合起来单个批次中具有不同颜色的对象。通过一点点工作,确实可以完成。
第21页 |
支持每个对象的唯一颜色的第一步是使其可以设置每个人的颜色。我们不能通过材料来做到这一点,因为这是一种资产对象都共享。让我们为它创建一个组件,命名为InstancedColor,给它一个可配置的色域。因为它不是我们的管道特定的,所以保持它的My Pipeline文件夹外的脚本文件。使用UnityEngine;公共类InstancedColor:MonoBehaviour {[ SerializeField ]颜色 = 颜色 .white;}要覆盖材质的颜色,我们必须提供对象的渲染器组件带有材料属性块。通过创建新的来做到这一点MaterialPropertyBlock对象例如,通过其给它一个_Color财产的setColor方法,然后将它传递给对象的MeshRenderer组件,通过调用其SetPropertyBlock 方法。我们假设在播放模式下颜色保持不变,所以在我们的Awake方法中这样做类。void Awake(){var propertyBlock = new MaterialPropertyBlock();propertyBlock.SetColor(“_ Color”,color);GetComponent< MeshRenderer>()SetPropertyBlock(propertyBlock)。}将我们的组件添加到场景中的一个对象。你会看到它的颜色更改,但只有在我们进入播放模式后。立方体与颜色组件。
第22页 |
要在编辑模式下立即查看场景中的颜色变化,请移动代码将颜色设置为OnValidate方法。的清醒然后方法可以简单地调用OnValidate 所以我们不需要重复代码。void Awake(){的OnValidate();}void OnValidate(){var propertyBlock = new MaterialPropertyBlock();propertyBlock.SetColor(“_ Color”,color);GetComponent <MeshRenderer>()SetPropertyBlock(propertyBlock)。}何时调用OnValidate ?OnValidate 是一种特殊的Unity消息方法。在编辑模式下调用它时组件已加载或更改。所以每次加载场景和编辑时组件。因此,各个颜色立即出现。将组件添加到所有形状,通过选择它们并添加一次,但是make一定不要再次将它添加到已有的对象中。也有他们所有使用相同的材料。替代材料可以删除,因为我们配置每个对象的颜色。许多颜色,一种材料。请注意,我们正在创建一个新的 每次我们设置颜色时, MaterialPropertyBlock实例覆盖。这不是必需的,因为每个网格渲染器都在内部跟踪重写的属性,从属性块复制它们。那意味着我们可以重复使用它,因此请跟踪单个静态块,仅在需要时创建它。
第23页 |
static MaterialPropertyBlock propertyBlock;...void OnValidate(){if (propertyBlock == null ){propertyBlock = new MaterialPropertyBlock();}propertyBlock.SetColor(“_ Color”,color);GetComponent< MeshRenderer>()SetPropertyBlock(propertyBlock)。}此外,我们可以通过预取它来略微加快颜色属性的匹配属性ID通过Shader .PropertyToID方法。每个着色器属性名称都有一个全局标识符整数。这些标识符可能会发生变化,但始终保留在单个会话期间保持不变,意味着在播放和编辑之间。所以我们获取一次,可以作为静态字段的默认值。static int colorID =Shader .PropertyToID(“_ Color”);...void OnValidate(){if (propertyBlock == null ){propertyBlock = new MaterialPropertyBlock();}propertyBlock.SetColor(colorID,color);GetComponent< MeshRenderer>()SetPropertyBlock(propertyBlock)。}3.5 每实例颜色覆盖每个对象的颜色会导致GPU实例中断。虽然我们正在使用单一材料,重要的是用于渲染的数据。因为我们已经被覆盖了每个对象的颜色,我们强制它们分开绘制。由于颜色差异而没有实例化。
第24页 |
我们的想法是将颜色数据放在一个数组中,这将使实例化再次发挥作用。我们的_Color属性必须与M矩阵一样处理。在这种情况下我们必须明确,因为核心库不会重新定义任意宏属性。相反,我们手动创建一个常量缓冲区实例化,通过UNITY_INSTANCING_BUFFER_START和随附的结束宏,将其命名为PerInstance以保持我们的命名方案一致。在缓冲区内,我们将颜色定义为UNITY_DEFINE_INSTANCED_PROP(float4,_Color)。当实例化不是使用的最终等于float4 _Color,但否则我们最终得到一个数组实例数据。// CBUFFER_START(UnityPerMaterial)// float4 _Color;// CBUFFER_ENDUNITY_INSTANCING_BUFFER_START (PerInstance)UNITY_DEFINE_INSTANCED_PROP (float4,_Color)UNITY_INSTANCING_BUFFER_END (PerInstance)为了处理现在可以定义颜色的两种可能方式,我们必须这样做通过UNITY_ACCESS_INSTANCED_PROP宏访问它,传递它我们的缓冲区和该物业的名称。个float4 UnlitPassFragment(VertexOutput 输入):SV_TARGET {return UNITY_ACCESS_INSTANCED_PROP(PerInstance,_Color);}现在,实例索引也必须在UnlitPassFragment中可用。所以加UNITY_VERTEX_INPUT_INSTANCE_ID 到VertexOutput,然后用UNITY_SETUP_INSTANCE_ID 在UnlitPassFragment 就像我们在 UnlitPassVertex中所做的那样。要做到这一点,我们必须复制从顶点输入到顶点输出的索引,我们可以使用它UNITY_TRANSFER_INSTANCE_ID 宏。
第25页 |
struct VertexInput {float4 pos:POSITION ;UNITY_VERTEX_INPUT_INSTANCE_ID};struct VertexOutput {float4 clipPos:SV_POSITION ;UNITY_VERTEX_INPUT_INSTANCE_ID};VertexOutput UnlitPassVertex(VertexInput 输入){VertexOutput 输出;UNITY_SETUP_INSTANCE_ID (输入);UNITY_TRANSFER_INSTANCE_ID (输入,输出);float4 worldPos =MUL( UNITY_MATRIX_M,个float4(input.pos。 XYZ,1.0));output.clipPos = mul(unity_MatrixVP,worldPos);返回输出;}个float4 UnlitPassFragment(VertexOutput 输入):SV_TARGET {UNITY_SETUP_INSTANCE_ID (输入);return UNITY_ACCESS_INSTANCED_PROP(PerInstance,_Color);}许多颜色,一个平局。所有对象现在最终组合在一个绘制调用中,即使它们都使用不同的颜色。但是,在常量缓冲区中可以放入多少数据是有限制的。最大实例批量大小取决于每个实例的数据变化量。除此之外,缓冲区最大值因平台而异。我们仍然只能使用相同的网格和材料。例如,混合立方体和球体将分裂批次。
第26页 |
立方体和球体。此时我们有一个可用于绘制许多对象的最小着色器尽可能高效。在未来,我们将在此基础上进一步创造更多高级着色器。下一个教程是Lights。知识库享受教程?它们有用吗?想要更多?请在Patreon上支持我!或者直接捐款!由Jasper Flick制作
暂无关于此日志的评论。