普通的卡通着色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()进行优化的原理:

在上面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​

汇编版本:

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

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

感觉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:维护

Unity Shader: 优化GPU代码--用step()代替if else等条件语句。相关推荐

  1. Unity Shader 常规光照模型代码整理

    Unity Shader 常规光照模型代码整理 本次整理在Unity中几种常见的光照模型,包含 1.BlinnPhong(常规光照模型) 2.ForwardRender(多灯光带有衰弱前向渲染) 3. ...

  2. 读书笔记_代码大全_第14章_组织直线型代码_第15章_使用条件语句

    组织直线型代码 + 使用条件语句 希望我的读书笔试能带你翻过18页的书 http://www.cnblogs.com/jerry19880126/ <代码大全>第14章和第15章的内容比较 ...

  3. python条件语句代码例子_Python 炫技操作:条件语句的七种写法

    原标题:Python 炫技操作:条件语句的七种写法 作者 | 写代码的明哥 责编 | 郭芮 有的人说 Python 入门容易,但是精通难的语言,这点我非常赞同. Python 语言里有许多(而且是越来 ...

  4. unity性能优化-GPU

    GPU:负责整个渲染流水线.它会从处理CPU传递过来的模型数据开始,进行Vertex Shader.Fragment Shader等一系列工作,最后输出屏幕上的每个像素.因此它的性能瓶颈包括顶点.像素 ...

  5. Unity Shader着色器优化

    对游戏开发者而言,着色器长久以来就是游戏开发中的重要部分,在Unity中编写并实现着色器的过程直观且高效,优秀的着色器还可以创造非常精美的游戏画面,同时保证极高的性能.今天将由Unity的技术工程师张 ...

  6. TA 认识 unity shader最基本的代码结构与书写01

    01:认识最简单的shader代码 Shader "Unlit/01minishader" { Properties { _MainTex ("Texture" ...

  7. Unity Shader - Built-in管线下优化 multi_compile_fwdbase、multi_compile_fog 变体

    文章目录 变体过多的缺点 项目情况 #pragma multi_compile_fwdbase 和 multi_compile_fog 生存的变体(keyword) 生存的变体 变体的数量 查看编译生 ...

  8. Unity Shader 学习笔记(3)URP渲染管线带阴影PBR-Shader模板(ASE优化版本)

    此 Shader 已经不是最新版本,最新版本见本专栏的第四篇文章: Unity Shader 学习笔记(4) 材质面板截图: 功能实现(URP渲染管线下): PBR材质.投射和接收阴影. 代码展示: ...

  9. unity性能优化 模型、贴图、shader优化方法大全

    优化,老生常谈.游戏的优化和网站.软件优化没有任何不同,除了编码质量和使用技巧以外,都是那些空间<>时间.效果<>性能的老套路. 带*号的,都是极其重要的优化手段,就算没用上你 ...

最新文章

  1. GIT中打标签(tag)的意义
  2. C++ GUI Qt4编程(12)-6.1FindFileDialog
  3. 大剑无锋之不都说面试不问mybatis,为什么我被虐了。。。【mybatis面试题】
  4. 设计模式之适配器模式--java描述
  5. Oracle转Sqlserver 记录
  6. java hdfs 指定用户目录_HDFS目录(文件 )权限管理
  7. python turtle库setpos_Python:使用mathturtle库绘制切线图
  8. .Net转Java自学之路—基础巩固篇二十(Lambda)
  9. 3D版pix2pix来了,画一只猫就能抱起来吸丨github
  10. 澎湖师傅共制巨型“米龟” 延续两岸“乞龟”祈福民俗
  11. 工具型产品的设计感想
  12. Roller的安装步骤
  13. 主引导记录mbr介绍
  14. Linux Mint(version19)
  15. 低成本、快速造测试数据,这个工具你指的拥有
  16. Element 根据勾选导出Excel表格数据
  17. android天气预报sdk,Android手机集成天气预报功能方案:全国天气预报API调用
  18. 如何用matlab实现小波变换
  19. Javabean,POJO,PO,VO,DTO
  20. 2014年国内餐饮O2O大事件

热门文章

  1. getBoundingClientRect()
  2. javascript判断日期奇偶_js中判断奇数或偶数
  3. php引擎,php基于什么引擎
  4. java五子棋棋盘_java五子棋项目(一)
  5. android 开启线程关闭对话框,java – 从后台线程的PopUp对话框Android
  6. centos系统中,相同的shell命令,直接在命令行中可以执行,在.sh文件中运行不了的原因
  7. 成田机场access西瓜卡_糟了,是心动的感觉!无限次机场/高铁接送,高端商旅神卡权益再升级...
  8. JAVA EE 6 jar包集合_Java EE6将JSF facelets(xhtml)和ManagedBeans打包成JAR
  9. matlab迭代xyz到blh,基于matlab的坐标转换精编.doc
  10. shell命令删除昨日的日志_linux定时自动清理日志文件