unity shader语言,Unity Shader: 优化GPU代码--用step()代替if else等条件语句。

 2023-09-23 阅读 25 评论 0

摘要:普通的卡通着色Shader: 先看一个Shader,卡通着色。由于卡通着色需要对不同渲染区域进行判定,比较适合做案例。 Shader "Unlit/NewToonShading" {Properties{_Shininess("Shininess",float)=1_Edge("Edge Scale",range(0,1))

普通的卡通着色Shader:

先看一个Shader,卡通着色。由于卡通着色需要对不同渲染区域进行判定,比较适合做案例。

Shader "Unlit/NewToonShading"
{Properties{_Shininess("Shininess",float)=1_Edge("Edge Scale",range(0,1))=0.2_FinalColor("Final Color",Color)=(0.5,0.5,0.5,1)_EdgeColor("Edge Color",Color)=(0,0,0,1)}SubShader{Tags { "RenderType"="Opaque"}LOD 100Pass{Tags {"LightMode"="Vertex" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float3 normal:NORMAL;};struct v2f{float4 vertex : SV_POSITION;float3 N:TEXCOORD0;float3 L:TEXCOORD1;float3 H:TEXCOORD2;float3 V:TEXCOORD3;};float _Shininess;float _Edge;float4 _FinalColor;float4 _EdgeColor;float4 _LightPosition_World;v2f vert (appdata v){v2f o=(v2f)0;float4 worldPos=mul(unity_ObjectToWorld,v.vertex);float4 lightPos_World=mul(UNITY_MATRIX_I_V,unity_LightPosition[1]);o.N=normalize(mul(unity_ObjectToWorld,v.normal));o.L=normalize(lightPos_World-worldPos.xyz);o.V=normalize(_WorldSpaceCameraPos-worldPos.xyz);o.H=normalize(o.L+o.V);o.vertex = UnityObjectToClipPos(v.vertex);return o;}fixed4 frag (v2f i) : SV_Target{i.N=normalize(i.N);i.L=normalize(i.L);i.H=normalize(i.H);i.V=normalize(i.V);float4 Kd=_FinalColor;	//diffuse漫反射光float4 Ks=0;			//specular高光fixed4 col;				//最终颜色//边缘判定float edge=max(dot(i.N,i.V),0);if(edge<_Edge){return _EdgeColor;	//返回一个描边颜色}//暗光判定float diffuseLight=max(dot(i.N,i.L),0);if(diffuseLight<=0.1f){		//暗光区域Kd*=0.5f;				//亮光区域亮度减半Ks=0;					//无高光。如果diffuseLight<=0,说明N,H夹角大于了90',眼睛或光源在材质表面后方,Ks也是0col=Kd+Ks;return col;}//高光判定float specularLight=pow(max(dot(i.N,i.H),0),_Shininess);if(specularLight>=0.95f){Ks=float4(1.0f,1.0f,1.0f,0.0f);		//高光由0变为高光颜色}col=Kd+Ks;return col;}ENDCG}}
}

这里写图片描述
(图1:NewToonShading渲染效果)

用step()进行优化的原理:

unity shader语言、在上面Shader的片段着色器中,我以正常cpu编程的逻辑进行了编程,例如,if(edge<_Edge){return _EdgeColor;},如果此像素被判定为边缘,则直接返回边缘颜色,那么则不用再进行之后的运算了。以此类推后面又用if else 分别进行了高光,亮光,暗光区的判断。但是这种优化对于gpu编程来讲是无效的。因为对于GPU来讲,各个顶点各个像素都在进行大量的并行运算,每个片段着色器都在同步运行,边缘地带像素的片段着色器虽然率先return,但是它依然要等待最后一个return的像素。只有所有像素全部完成计算,才会进行下一次运算, 在片段着色器中,每个片段处理器每条指令操作上百个像素,如果有些片段(像素)采取一个分支而有些片段不采用另一个分支,则所有片段都会执行两个分支,但只在每个片段应该采取的分支上写入寄存器。不论何种策略,对追求高并行度的GPU来讲,分支是必须要同步的,那么最慢的case就会造成短板效应。另外,if/endif等流程控制操作对GPU来讲有较高的指令开销(4个时钟周期,Geforce6)修改1_{修改1}1。因此在GPU编程中,if else ,switch case和嵌套if语句等等是不推荐的会影响GPU的工作效率。相应的,可以用step()等函数进行替换,用阶梯函数的思维来构建条件语句。这样,所有的线程都执行完全一样的代码,加大了并行化计算的可能性,消除条件分支指令的性能损耗,在很多方面对GPU都是有益的。

Step()版本:

Shader "Unlit/NewToonShading_StepVersion"
{Properties{_Shininess("Shininess",float)=1_Edge("Edge Scale",range(0,1))=0.2_FinalColor("Final Color",Color)=(0.5,0.5,0.5,1)_EdgeColor("Edge Color",Color)=(0,0,0,1)}SubShader{Tags { "RenderType"="Opaque"}LOD 100Pass{Tags {"LightMode"="Vertex" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float3 normal:NORMAL;};struct v2f{float4 vertex : SV_POSITION;float3 N:TEXCOORD0;float3 L:TEXCOORD1;float3 H:TEXCOORD2;float3 V:TEXCOORD3;};float _Shininess;float _Edge;float4 _FinalColor;float4 _EdgeColor;float4 _LightPosition_World;v2f vert (appdata v){v2f o=(v2f)0;float4 worldPos=mul(unity_ObjectToWorld,v.vertex);float4 lightPos_World=mul(UNITY_MATRIX_I_V,unity_LightPosition[1]);o.N=normalize(mul(unity_ObjectToWorld,v.normal));o.L=normalize(lightPos_World-worldPos.xyz);o.V=normalize(_WorldSpaceCameraPos-worldPos.xyz);o.H=normalize(o.L+o.V);o.vertex = UnityObjectToClipPos(v.vertex);return o;}fixed4 frag (v2f i) : SV_Target{i.N=normalize(i.N);i.L=normalize(i.L);i.H=normalize(i.H);i.V=normalize(i.V);float4 Kd=_FinalColor;float4 Ks=0;fixed4 col;//边缘判定float edge=max(dot(i.N,i.V),0);edge=step(edge,_Edge); //if(edge<=_Edge) edge=1 , else edge=0_EdgeColor*=edge;//高光判定float specularLight=pow(max(dot(i.N,i.H),0),_Shininess);specularLight=step(0.95f,specularLight);		//if specularLight>=0.95f specularLight=1 else =0//暗光判定float diffuseLight=max(dot(i.N,i.L),0);diffuseLight=step(0.1f,diffuseLight); //if(diffuseLight>=0.1f) diffuseLight=1   else diffuseLight=0Ks=specularLight*diffuseLight;		//if diffuseLight=0, Ks=0; else Ks=specularLight(1 or 0)diffuseLight=diffuseLight*0.5f+0.5f;	  //change 1 or 0 to 1 or 0.5//0.5Kd or Kd  1or0  1or0       0or1	  0orEdgeColor	col=(Kd*diffuseLight+Ks)*(1.0f-edge)+_EdgeColor;		return col;}ENDCG}}
}

举例解释:

在HLSL中, step(a,b)既是当b>=a时返回1,否则返回0,换句话说既是当a<=b时返回1,否则返回0。因此可以把被比较数灵活的插入a或b的位置,完成小于或大于的比较。由于返回值是0或1,它无法直观的替代if else逻辑判断,需要结合改造算法,例如:

				//边缘判定float edge=max(dot(i.N,i.V),0);if(edge<_Edge){return _EdgeColor;}

上文中,直接返回的_EdgeColor,将在下文中变为一个000或保持自身值的rgb变量,新增的一个edge变量会变为0或1的开关,并在最后的计算步骤中参与最终颜色的计算:

				//边缘判定float edge=max(dot(i.N,i.V),0);edge=step(edge,_Edge); //if(edge<=_Edge) edge=1 , else edge=0_EdgeColor*=edge;//...中间过程略...//0.5Kd or Kd  1or0		1or0       0or1	  0orEdgeColor	col=(Kd*diffuseLight+Ks)*(1.0f-edge)+_EdgeColor;

代码优化方法不包括,如果此当前像素为边缘,edge为1,那么在最终颜色计算中,不论其他变量如何,它都会变为一个0+_EdgeColor的值,既是边缘颜色。如果此像素为非边缘地带,edge为0,_EdgeColor为0,那么最终颜色为 “其他颜色”*1+0,边缘颜色被剔除。

以此类推,原版中高光,亮光与暗光区域判断的返回值也都变成了参与到最终颜色计算中的变量。具体逻辑可见step()版本各行后面注释。

测试

这里写图片描述
(图2:NewToonShading与NewToonShading_StepVersion渲染效果比较)

代码优化的主要工作有哪些,这里写图片描述
(图3:NewToonShading与NewToonShading_StepVersion渲染效果比较)

两个版本的FPS波动范围基本相同,有可能是计算量太小或此Shader内容对此问题不太敏感,但起码证明if else版本按照CPU的思维提前返回相对于step()版本进行所有的计算是无起到任何优势的。 第一是if分支内计算量较小,未造成太明显的短板效应与帧速瓶颈。第二可能是step版本虽省去了分支指令,但是增加了计算指令,抵消后优化效果过于微弱。修改2_{修改2}2

汇编版本:

汇编后的片段着色器代码(部分截取):

unity 性能优化?if else版本:

   0: dp3 r0.x, v1.xyzx, v1.xyzx1: rsq r0.x, r0.x2: mul r0.xyz, r0.xxxx, v1.xyzx3: dp3 r0.w, v4.xyzx, v4.xyzx4: rsq r0.w, r0.w5: mul r1.xyz, r0.wwww, v4.xyzx6: dp3 r0.w, r0.xyzx, r1.xyzx7: max r0.w, r0.w, l(0.000000)8: lt r0.w, r0.w, cb0[2].y9: if_nz r0.w10:   mov o0.xyzw, cb0[4].xyzw11:   ret 12: endif 13: dp3 r0.w, v2.xyzx, v2.xyzx14: rsq r0.w, r0.w15: mul r1.xyz, r0.wwww, v2.xyzx16: dp3 r0.w, r0.xyzx, r1.xyzx17: max r0.w, r0.w, l(0.000000)18: ge r0.w, l(0.100000), r0.w19: if_nz r0.w20:   mul o0.xyzw, cb0[3].xyzw, l(0.500000, 0.500000, 0.500000, 0.500000)21:   ret 22: endif 23: dp3 r0.w, v3.xyzx, v3.xyzx24: rsq r0.w, r0.w25: mul r1.xyz, r0.wwww, v3.xyzx26: dp3 r0.x, r0.xyzx, r1.xyzx27: max r0.x, r0.x, l(0.000000)28: log r0.x, r0.x29: mul r0.x, r0.x, cb0[2].x30: exp r0.x, r0.x31: ge r0.x, r0.x, l(0.950000)32: and r0.xyzw, r0.xxxx, l(0x3f800000, 0x3f800000, 0x3f800000, 0)33: add o0.xyzw, r0.xyzw, cb0[3].xyzw34: ret 

第9和第19行两个if_nz分支指令。从第31行 ge r0.x, r0.x, l(0.950000) 看来编译器是把第三个if分支优化掉了。

step()版本:

   0: dp3 r0.x, v3.xyzx, v3.xyzx1: rsq r0.x, r0.x2: mul r0.xyz, r0.xxxx, v3.xyzx3: dp3 r0.w, v1.xyzx, v1.xyzx4: rsq r0.w, r0.w5: mul r1.xyz, r0.wwww, v1.xyzx6: dp3 r0.x, r1.xyzx, r0.xyzx				//dot(i.N,i.H)7: max r0.x, r0.x, l(0.000000)				//max(dot(i.N,i.H),0)8: log r0.x, r0.x							//9: mul r0.x, r0.x, cb0[2].x					//pow(max(dot(i.N,i.H),0),_Shininess);10: exp r0.x, r0.x							//11: ge r0.x, r0.x, l(0.950000)				//specularLight=step(0.95f,specularLight);	12: dp3 r0.y, v2.xyzx, v2.xyzx13: rsq r0.y, r0.y14: mul r0.yzw, r0.yyyy, v2.xxyz15: dp3 r0.y, r1.xyzx, r0.yzwy				//dot(i.N,i.L)16: max r0.y, r0.y, l(0.000000)				//float diffuseLight=max(dot(i.N,i.L),0);17: ge r0.y, r0.y, l(0.100000)				//diffuseLight=step(0.1f,diffuseLight); 18: and r0.xz, r0.xxyx, l(0x3f800000, 0, 0x3f800000, 0)19: movc r0.y, r0.y, l(1.000000), l(0.500000)20: mul r0.x, r0.z, r0.x21: mad r0.xyzw, cb0[3].xyzw, r0.yyyy, r0.xxxx22: dp3 r1.w, v4.xyzx, v4.xyzx23: rsq r1.w, r1.w24: mul r2.xyz, r1.wwww, v4.xyzx25: dp3 r1.x, r1.xyzx, r2.xyzx				//dot(i.N,i.V)26: max r1.x, r1.x, l(0.000000)				//float edge=max(dot(i.N,i.V),0);27: ge r1.x, cb0[2].y, r1.x					//edge=step(edge,_Edge);28: movc r0.xyzw, r1.xxxx, l(0,0,0,0), r0.xyzw29: and r1.x, r1.x, l(0x3f800000)30: mad o0.xyzw, cb0[4].xyzw, r1.xxxx, r0.xyzw31: ret 

unity gpu优化。感觉edge的计算好像是移到了后面(25-27)。8-10用log,mul,exp怎么实现源码的pow()没太看懂。step()的实现都是用的一条ge指令:

格式:ge dest src0 src1

做src0 >= src1比较,如果为真,0xFFFFFFFF写入到dest,否则写入0x0000000。

————————————————————————————————
参考:
GPU gems 2 - Nvidia
Microsoft HLSL: https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/ge–sm4—asm-
维护日志:
2017-9-20:修改1,修改2
2020-8-16:维护

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/92302.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息