【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

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

MySQL中慢SQL优化的不同方式介绍

《MySQL中慢SQL优化的不同方式介绍》慢SQL的优化,主要从两个方面考虑,SQL语句本身的优化,以及数据库设计的优化,下面小编就来给大家介绍一下有哪些方式可以优化慢SQL吧... 目录避免不必要的列分页优化索引优化JOIN 的优化排序优化UNION 优化慢 SQL 的优化,主要从两个方面考虑,SQL 语

MySQL中慢SQL优化方法的完整指南

《MySQL中慢SQL优化方法的完整指南》当数据库响应时间超过500ms时,系统将面临三大灾难链式反应,所以本文将为大家介绍一下MySQL中慢SQL优化的常用方法,有需要的小伙伴可以了解下... 目录一、慢SQL的致命影响二、精准定位问题SQL1. 启用慢查询日志2. 诊断黄金三件套三、六大核心优化方案方案

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

使用国内镜像源优化pip install下载的方法步骤

《使用国内镜像源优化pipinstall下载的方法步骤》在Python开发中,pip是一个不可或缺的工具,用于安装和管理Python包,然而,由于默认的PyPI服务器位于国外,国内用户在安装依赖时可... 目录引言1. 为什么需要国内镜像源?2. 常用的国内镜像源3. 临时使用国内镜像源4. 永久配置国内镜

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3