【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

相关文章

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

Python多线程应用中的卡死问题优化方案指南

《Python多线程应用中的卡死问题优化方案指南》在利用Python语言开发某查询软件时,遇到了点击搜索按钮后软件卡死的问题,本文将简单分析一下出现的原因以及对应的优化方案,希望对大家有所帮助... 目录问题描述优化方案1. 网络请求优化2. 多线程架构优化3. 全局异常处理4. 配置管理优化优化效果1.

Java List 使用举例(从入门到精通)

《JavaList使用举例(从入门到精通)》本文系统讲解JavaList,涵盖基础概念、核心特性、常用实现(如ArrayList、LinkedList)及性能对比,介绍创建、操作、遍历方法,结合实... 目录一、List 基础概念1.1 什么是 List?1.2 List 的核心特性1.3 List 家族成

MySQL中优化CPU使用的详细指南

《MySQL中优化CPU使用的详细指南》优化MySQL的CPU使用可以显著提高数据库的性能和响应时间,本文为大家整理了一些优化CPU使用的方法,大家可以根据需要进行选择... 目录一、优化查询和索引1.1 优化查询语句1.2 创建和优化索引1.3 避免全表扫描二、调整mysql配置参数2.1 调整线程数2.

Python学习笔记之getattr和hasattr用法示例详解

《Python学习笔记之getattr和hasattr用法示例详解》在Python中,hasattr()、getattr()和setattr()是一组内置函数,用于对对象的属性进行操作和查询,这篇文章... 目录1.getattr用法详解1.1 基本作用1.2 示例1.3 原理2.hasattr用法详解2.