【UnityShader入门精要学习笔记】第五章(2)优化你的Shader

2024-02-20 14:04

本文主要是介绍【UnityShader入门精要学习笔记】第五章(2)优化你的Shader,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

总之适用于同样开始学习Shader的同学们进行有取舍的参考。


文章目录

  • 复习
    • 知识点复习
  • 如何Debug
    • 使用假彩色图像
    • 使用Visual Studio
    • FrameDebugger
    • 小心渲染平台差异
    • Shader的语法差异
  • Shader整洁之道
    • 浮点类型
    • 规范语法
    • 避免不必要的计算
    • 慎用分支和循环语句
    • 不要除以0


复习

知识点复习

在上节课中,我们编写了一个Shader并将其由简单逐步完善,从中我们理解了Shader的基本使用方法:

  1. shader的结构包括 Shader Name,SubShader块,pass块FallBack四部分
  2. 我们通过预定义指令 #pragma 指定不同着色器阶段使用的函数,在Shader中我们可以通过语义来为变量赋值,一些语义让我们访问Untiy提供的数据。而一些语义具有特殊含义,在定义着色器函数的时候必须使用,例如a2v阶段的输出需要使用语义SV_POSITION,而v2f阶段的输出需要使用语义SV_Target
  3. 我们可以自定义着色器函数的输入和输出,通过自定义结构体来包含我们需要使用的变量集合。
  4. 我们可以通过#include "XXX.cginc"代码,来包含unity提供的.cginc头文件,使用内置的函数(例如UnityObjectToClipPos)和变量便利Shader代码的编写。
  5. 想要在材质面板上使用属性,我们需要在Properties 中定义属性及其默认值,并在SubShader块中定义其对应的变量并在函数中进行处理。

如何Debug

对一个Shader进行调试是十分困难的,如果发现效果不对,我们可能要花非常多的时间来找到问题所在,因为在Shader中可以选择的调试方式非常优先,甚至连简单的输出都不行。通常的调试方法,例如print出关键数据,在编译器里找报错,断点调试这些在Shader中都做不到。
下面介绍了两种对Shader的调试方法:

使用假彩色图像

假彩色图像指的是将需要调试的变量映射到[0,1]之间,将它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。由于我们没法打印Shader中的值,只能将值作为颜色输出进行判断。

由于颜色值的分量在 [ 0 , 1 ] [0,1] [0,1],因此我们需要将要调试的值域范围映射为 [ 0 , 1 ] [0,1] [0,1]之后再输出,如果你不了解一个变量的范围,这说明你不了解shader中的计算,就只能不停尝试,一个提示是:颜色分量中任何大于1的值都会被设置为1,而小于0的值都会设置为0。因此如果不断尝试映射方式也能够找出颜色范围。

如果需要调试的是一个一维数据,那么可以选择一个单独的颜色分量(R分量)进行输出,其他分量置为0,(当然a分量是1)。如果是多维数据,则可以选择对它的每一个分量单独调试,或者一次性输出多个颜色分量。

在Github的项目demo的Scence5_2中展示了使用假彩色图像可视化一些模型数据的方法,如法线,切线,纹理坐标,顶点颜色,以及他们的运算结果等等:

Shader "Unity Shaders Book/Chapter 5/False Color" {SubShader {Pass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float4 pos : SV_POSITION;fixed4 color : COLOR0;};v2f vert(appdata_full v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);// 可视化法线方向o.color = fixed4(v.normal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化切线方向o.color = fixed4(v.tangent.xyz * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化副切线方向fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;o.color = fixed4(binormal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);// 可视化第一组纹理坐标o.color = fixed4(v.texcoord.xy, 0.0, 1.0);// 可视化第二组纹理坐标o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);// 可视化第一组纹理坐标的小数部分o.color = frac(v.texcoord);if (any(saturate(v.texcoord) - v.texcoord)) {o.color.b = 0.5;}o.color.a = 1.0;// 可视化第二组纹理坐标的小数部分o.color = frac(v.texcoord1);if (any(saturate(v.texcoord1) - v.texcoord1)) {o.color.b = 0.5;}o.color.a = 1.0;// 可视化顶点颜色
//				o.color = v.color;return o;}fixed4 frag(v2f i) : SV_Target {return i.color;}ENDCG}}
}

其中用到的appdata_full 结构体是Unity内置的结构体,可以在UnityCG.cginc中找到它的定义:

struct appdata_full{float4 vertex:POSITION;float4 tangent : TANGENT;float3 normal: NORMAL;float4 texcoord : TEXCOORD0;float4 texcoord1 : TEXCOORD1;float4 texcoord2 : TEXCOORD2;float4 texcoord3 : TEXCOORD3;
#if defined(SHADER_API XBOX360)half4 texcoord4 : TEXCOORD4;half4 texcoord5 : TEXCOORD5;
#endiffixed4 color : COLOR;
}

可以看出,appdata_full 结构体几乎包含了所有的模型数据,和其命名也相称,代表了从应用阶段能够给出的所有模型数据。

在这里插入图片描述
用项目中自带的拾色器脚本,来检测屏幕的RGB假彩色图像值。


使用Visual Studio

VS中也提供了一种对UnityShader的调试功能——Graphics Debugger

其原理是截取屏幕上帧并通过GPU反编译获取Shader信息。

通过“视图”->“选项”->“图形诊断”->“启用 GPU 反汇编收集”来启用 GPU 反汇编。
若要使更改生效,必须重新打开 vsglog。


FrameDebugger

在这里插入图片描述
或者我们也可以使用Unity原生的帧调试器,它可以用于查看渲染该帧时进行的各种渲染事件(Event),包括DrawCall序列,或者清空帧缓存等操作。
在这里插入图片描述
通过最上方滑动条检查渲染时发生的顺序事件。我们可以通过前进后退按钮查看事件的发生。

在这里插入图片描述

左侧是事件树,我们可以看到不同阶段都是什么事件。右侧则展示了事件的参数。通常以Draw开头的事件都是一个DrawCall,一个Gameobject的DrawCall渲染结果会在Hierarchy面板上高亮显示。

如果选中的DrawCall是对一个渲染纹理(RenderTexture)的渲染操作,那么这个渲染纹理就可以在Game视图中看到。

帧调试器的原理其实比较简单,就是用停止渲染的方法来查看事件渲染的结果。


小心渲染平台差异

在这里插入图片描述
之前我们讲过OpenGL和DirectX的屏幕坐标空间是不同的,在渲染时,我们不仅可以把渲染结果输出到屏幕上,还可以输出到不同的渲染目标(Render Target)。大多数情况下,这样的差异不会造成任何影响。但当我们要使用渲染到纹理技术(Render Texture)时,如果不采取任何措施,就会出现纹理翻转的情况,不过大部分情况下Unity会根据平台类型为我们自动翻转屏幕图像纹理(Graphic.Blit(source, dest)方法),所以直接导出游戏时不会因为平台不同导致渲染问题了。

但是如果开启了抗锯齿后并在此时使用了渲染纹理技术,在这种情况下,Unity首先渲染得到屏幕图像,再由硬件进行抗锯齿处理后,得到一张渲染纹理来供我们进行后续处理。在这种情况下,Unity就不能在渲染纹理时为我们翻转图像纹理了。而如果只处理一张渲染图像时仍然不影响,只有当我们开启了抗锯齿,且需要处理多张图像,且在DirectX平台上,才有可能出现不同渲染图像的纹理坐标不同的情况。

当出现上述情况,我们就需要手动翻转顶点着色器中的纹理坐标:

#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)uv.y = 1 - uv.y;
#endif

其中,UNITY_UV_STARTS_AT_TOP用于判断是否是DirectX平台,如果是,我们通过_MainTex_TexelSize.y 来判断是否开启了抗锯齿,若<0代表了开启,此时则手动翻转UV。(有时上述代码可能出现问题,处理后反而颠倒了,往往是因为Graphic.Blit方法的使用,如果发生问题就往这个思路去Debug)


Shader的语法差异

在不同平台的Shader有语法差异,有时会因为某些平台对Shader的语义严格而造成问题,因此我们定义时需要使用严格的语法,一般的要求是对使用的变量进行符合格式的初始化,例如:

incorrect number of arguments to numeric-type constructor (compiling for d3dll)(不同平台对语义检查更严格,导致编译错误)

简略的定义,在某些平台可能报错
float4 v = float4(0.0);
完整定义
float4 v = float4(0.0, 0.0, 0.0, 0.0);

output parameter ' o ' not completely ini alized {compi ng for d3dll)(报错原因是由于使用变量o时未初始化赋值,导致编译错误)

void vert (inout appdata_full v , out Input o) {// 使用Unity内置的UNITY_INITIALIZE_OUTPUT宏对输出结构体o进行初始化UNITY_INITIALIZE_OUTPUT(Input,o);// ...
}

除此之外,tex2D是一个对纹理进行采样的函数,但在DIrectX 9/11也是不支持的,需要使用tex2Dlod来代替

tex2Dlod(tex, float4(uv, 0, 0))

在之前章节中我们也说过不同语义在不同平台可能不能编译,因此推荐使用SV_POSITION,SV_Target语义来描述输入输出变量。


Shader整洁之道

本章给出了一些写出规范Shader代码的建议,整洁的代码是提升效率所必须的。

浮点类型

在CG/HLSL中,有三种精度类型的数值:float,half和fixed。这些精度将决定计算结果的数值范围。

类型精度
float32位浮点数
half16位浮点数
fixed11位浮点数

大多数GPU对不同类型都会按照最高精度进行计算,特别在PC平台上很呐看出不同精度的区别,而在移动平台的GPU上可能会有些差异。

一个基本的建议是,尽可能使用精度较低的类型,这样可以优化Shader的性能。我们可以用fixed类型来存储颜色和单位向量。

规范语法

规范语法的要求无非之前讲到的几点:

  • 进行严格的语法定义
  • 使用变量前保证初始化
  • 使用SV_POSITION,SV_Target等语义

避免不必要的计算

如果我们毫无节制地在Shader中(尤其是片元着色器)中进行了大量计算,那么可能会报错:
temporary register limit of 8 exceeded

'Arithmetic instruction limit of 64 exceeded; 65 arithmetic instructions needed to compile program

出现这些信息往往是因为我们在Shader中进行了过多的运算,使得需要的临时寄存器数目或者指令数目超过了当前可支持的数目。

不同的Shader Target、不同的着色器阶段,我们可以使用的临时寄存器和指令数目都是不同的。这些往往是由Shader Model的等级来决定的。Shader Model的等级越高,Shader能使用的运算指令数目、寄存器个数等特性和能力越强。

使用Shader Target宏可以指定使用的Shader Model等级:

指令描述
#pragma target 2.0默认的Shader Target等级,相当于Direct3D 9上的ShaderModel 2.0
#pragma target 3.0相当于Direct3D 9上的ShaderModel 3.0
#pragma target 4.0相当于Direct3D 10上的ShaderModel 4.0 (只在DirectX 11 和Xbox One/PS4 平台上支持)
#pragma target 5.0相当于Direct3D 11上的ShaderModel 5.0 (只在DirectX 11 和Xbox One/PS4 平台上支持)

慎用分支和循环语句

使用分支和循环语法,在其他高级语言中是基础中的基础了,但是在编写Shader的时候,我们要对它们格外小心。

在Shader上使用这些流程控制语句,往往导致Shader的性能下降,一个解决方法是:我们应该将这些计算向流水线上端移动,例如把放在片元着色器中的计算放到顶点着色器中,或者直接放在CPU中计算,再把结果传递给Shader。

如果真的不可避免需要使用分支语句进行计算,那么一些建议是:

  • 分支判断语句使用的条件变量最好是常数,即在Shader运行过程中不会变换
  • 每个分支中包含的操作指令数尽可能少
  • 分支的嵌套层数尽可能少
  • 如果某些库已经实现了宏或者方法,就直接去用它们的方法,尽量避免自己实现

不要除以0

在编译Shader的时候,我们可能会忘记不能除以0这个基本常识,更糟糕的是编译器可能不会报错,这些代码的结果往往是不可预测的:

fixed4 frag(v2f i):SV_Target
{return fixed4(0.0/0.0, 0.0/0.0, 0.0/0.0, 1.0)
}

这些代码可能导致Shader的崩溃,即使不崩溃,得到的结果也是不确定的。

一个解决方法是,对那些除数可能为0的情况,强制截取到非0范围。

这篇关于【UnityShader入门精要学习笔记】第五章(2)优化你的Shader的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/728458

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份