首先欢迎大家关注订阅我的小专栏,里面会定时更新一些游戏开发的内容。
网址:https://xiaozhuanlan.com/xiaochengzi
本专栏讲述利用Unity进行游戏开发使用到的一些热门技术、框架、shader、优化技巧等等,偏向在工作中的实际应用,也为广大游戏开发者提供一些宝贵的经验。
专栏内容:
1.unity优化技巧
2.工作中常用的一些热门通用技术。
3.unity shader 讲解
4.游戏框架
5.unity一些好用的插件、组件的介绍
6.自己写的一些辅助插件、辅助工具
7.安卓、IOS打包、真机调试、Profiler性能分析技巧专讲
8.Jenkins自动化构建出包(编写Windows批处理命令)
9.游戏中的帧同步、状态同步技术专讲
持续更新。。。
关于我:
目前在职盛趣游戏(原盛大游戏),担任游戏客户端程序开发一职,在职1年多。
ShaderForge是适用于Unity,基于节点的着色器编辑器 。它可让你更轻松地创建自己的着色器 ,而不需要使用代码。只需要经过连线编译,就能很好的实现一些基于材质shader的效果或特效。用来做一些简单的特效,亦或是学习了解shader的语法,都是一个很好的选择。
以下是做的一些效果图,我将以卡通效果和屏幕等宽的outline来进行相关研究介绍。
一.什么是屏幕等宽的描边(outline)?
屏幕等宽是指无论离摄像机镜头远近都是固定的像素,作为描边宽度。shaderforge是有描边节点的,只需要设置描边宽度和颜色,就可以实现描边了。但是为什么不用它自带的呢?
因为它不是屏幕等宽的描边,当你的镜头离模型很近时,他的描边也会变粗,如下图:
这样会影响观察效果,所以在大多情况下,我们应该无论离摄像机镜头远近都是固定的像素的描边宽度,这样才是合理的。如下图:
二.修改shaderforge的源代码,创建自定义节点、属性
用过shaderforge的同学应该都知道,这个插件做出来的shader只需要在可视化界面中做做连线,修改参数等操作就可以做出一些不错的效果了,拿卡通效果和描边来说,如下图:
但是当你需要实现一些其他效果或者方案时,如果你找不到相关节点,比如我要做屏幕等宽的描边,但是右边的main节点中的outlineWidth是非屏幕等宽的描边。那么该怎么办?两种方案解决。
1.直接打开该shader,修改名字为outline的pass块,把自动生成的非等宽描边的计算删掉,重新写一个屏幕等宽的描边计算公式,代码如下:
Pass {
Name "Outline"
Tags {
}
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_shadowcaster
#pragma multi_compile_fog
#pragma only_renderers d3d9 d3d11 glcore gles gles3
#pragma target 3.0
uniform float _OutLineSlider;
uniform float4 _OutLineColor;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 pos : SV_POSITION;
UNITY_FOG_COORDS(0)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos( float4(v.vertex.xyz + v.normal*lerp(0,0.05,_OutLineSlider),1) );
o.pos = UnityObjectToClipPos( v.vertex );
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
float4 frag(VertexOutput i) : COLOR {
return fixed4(_OutLineColor.rgb,0);
}
ENDCG
}
这段代码是当有连线连到outlineWidth和color时shaderforge自动生成的outline描边的代码pass块,然后你的模型就会显示一个描边了,宽度和颜色可以在shader面板中根据需要调节。
如果要实现等宽描边,把这块删掉重写(主要是对顶点、片元函数的代码块的修改,其他属性基本不变),代码如下:
Shader "Shader Forge/Toon03" {
Properties {
_Texture ("Texture", 2D) = "white" {}
_OutlineColor ("OutlineColor", Color) = (0,0,0,1)
_OutlineWidth("OutlineWidth",Range(0, 1))=0.01
_SepColor ("SepColor", 2D) = "white" {}
}
SubShader {
Cull Off
//使用cull front把外表面裁掉
Tags {
"RenderType"="Opaque"
"Queue" = "Geometry"
}
Pass {
Name "Outline"
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_shadowcaster
#pragma multi_compile_fog
#pragma only_renderers d3d9 d3d11 glcore gles gles3
#pragma target 3.0
uniform float4 _OutlineColor;
uniform float _OutlineWidth;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 pos : SV_POSITION;
UNITY_FOG_COORDS(0)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.5;
//这两行代码就是保持边缘曲线远近距离大小一样的关键
float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
pos = pos + float4(normalize(normal), 0) * _OutlineWidth * 0.01 * dist;
o.pos = mul(UNITY_MATRIX_P, pos);
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
float4 frag(VertexOutput i) : SV_Target{
return float4(_OutlineColor.rgb, 1);
}
ENDCG
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma multi_compile_fog
#pragma only_renderers d3d9 d3d11 glcore gles gles3
#pragma target 3.0
uniform sampler2D _Texture; uniform float4 _Texture_ST;
uniform sampler2D _SepColor; uniform float4 _SepColor_ST;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
LIGHTING_COORDS(2,3)
UNITY_FOG_COORDS(4)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.pos = UnityObjectToClipPos( v.vertex );
UNITY_TRANSFER_FOG(o,o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
float3 normalDirection = i.normalDir;
float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
float3 halfDirection = normalize(viewDirection+lightDirection);
////// Lighting:
float attenuation = LIGHT_ATTENUATION(i);
float node_1974 = max(0,dot(halfDirection,i.normalDir));
float2 node_1679 = float2(node_1974,node_1974);
float4 _SepColor_var = tex2D(_SepColor,TRANSFORM_TEX(node_1679, _SepColor));
float node_6399 = (max(0,dot(i.normalDir,lightDirection))*attenuation);
float2 node_3750 = float2(node_6399,node_6399);
float4 _Texture_var = tex2D(_Texture,TRANSFORM_TEX(node_3750, _Texture));
float3 finalColor = (_SepColor_var.rgb+_Texture_var.rgb);
fixed4 finalRGBA = fixed4(finalColor,1);
UNITY_APPLY_FOG(i.fogCoord, finalRGBA);
return finalRGBA;
}
ENDCG
}
Pass {
Name "FORWARD_DELTA"
Tags {
"LightMode"="ForwardAdd"
}
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDADD
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdadd_fullshadows
#pragma multi_compile_fog
#pragma only_renderers d3d9 d3d11 glcore gles gles3
#pragma target 3.0
uniform sampler2D _Texture; uniform float4 _Texture_ST;
uniform sampler2D _SepColor; uniform float4 _SepColor_ST;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
LIGHTING_COORDS(2,3)
UNITY_FOG_COORDS(4)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.pos = UnityObjectToClipPos( v.vertex );
UNITY_TRANSFER_FOG(o,o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
float3 normalDirection = i.normalDir;
float3 lightDirection = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz,_WorldSpaceLightPos0.w));
float3 halfDirection = normalize(viewDirection+lightDirection);
////// Lighting:
float attenuation = LIGHT_ATTENUATION(i);
float node_1974 = max(0,dot(halfDirection,i.normalDir));
float2 node_1679 = float2(node_1974,node_1974);
float4 _SepColor_var = tex2D(_SepColor,TRANSFORM_TEX(node_1679, _SepColor));
float node_6399 = (max(0,dot(i.normalDir,lightDirection))*attenuation);
float2 node_3750 = float2(node_6399,node_6399);
float4 _Texture_var = tex2D(_Texture,TRANSFORM_TEX(node_3750, _Texture));
float3 finalColor = (_SepColor_var.rgb+_Texture_var.rgb);
fixed4 finalRGBA = fixed4(finalColor * 1,0);
UNITY_APPLY_FOG(i.fogCoord, finalRGBA);
return finalRGBA;
}
ENDCG
}
}
FallBack "Diffuse"
CustomEditor "ShaderForgeMaterialInspector"
}
这是完整的shader代码,包括卡通效果和描边处理。(重点看outline的pass块里面的顶点函数和片元函数,主要这边的算法不一样)。
2.第二种方案,就是这篇文章的重点了。大家导入shaderforge插件后,会看到根目录下面有一个shaderforge.dll文件。这个dll就是shaderforge的源代码生成的dll。里面包含了shaderforge的所有内容(创建节点、连线事件、自动生成shader代码等等)。你把所有其他与shaderforge有关的文件和资源全部删掉,只留下这个dll就能进行可视化shader制作了。
所以,我们要做的就是找到shaderforge的源代码,在main节点下创建一个outlineEqualWidth的属性节点,然后连线到这个节点后,能够自动生成等宽描边的shader代码,这就完成了。最后,改完源代码,创建一个新的工程,把源代码导入进去,生成dll,替换原来的dll就完事了,自定义的等宽描边就可以使用了!(下图的倒数第5个属性节点)
三、修改源代码的步骤:
shaderforge的源码在GitHub上就有,文件夹名字叫code,在ShaderForge-》Editor目录下,和InternalResourse同级目录,有的插件在Editor目录下会放置一个源代码的压缩包,解压后源代码也会出来。
(注意:如果源代码和dll都在unity工程里,会报很多错误,同类代码冲突。所以,先把shaderforge.dll给删除,后面我们生成自己的dll,删除后只要源代码还在,之前做的效果是不会出问题的,这俩本来就是一个东西)
shaderforge的源代码研究起来不简单,注释很少,没也有相关教程和文档,所以只能摸索研究了,所以希望这篇博客对其他同学有所帮助。
1.首先找到SF_FeatureChecker.cs脚本,这个脚本是定义和创建main节点里各个属性。
在这里加上outlineEqualWidth字段。定义等宽描边属性
在 Initialize函数中创建等宽描边属性节点(直接复制outlineWidth的那行代码,把strID(olewid)这个改了就行,其他的参数不变)。这下在可视化视图中就有outlineEqualWidth了。
现在连线到这个节点后还没有任何效果,我们继续来。
2.找到SF_FeatureChecker.cs脚本。在UpdateAvailability() 函数中修改一行代码,然后添加一行代码。
修改的代码:editor.mainNode.outlineColor.SetAvailable((editor.mainNode.outlineEqualWidth.IsConnectedAndEnabled()||editor.mainNode.outlineWidth.IsConnectedAndEnabled()) && !deferredPp);
这行加上 editor.mainNode.outlineEqualWidth.IsConnectedAndEnabled()。的判断,不然outlineColor不能与我们自定义描边进行融合处理。
添加的代码:
editor.mainNode.outlineEqualWidth.SetAvailable(!deferredPp);
这样当我们连线到这个节点属性时,就可以有后续交互了。
3.找到SF_PassSetting.cs脚本。加上一个字段,后面进行计算描边的时候会得到连线到outlineEqualWidth的属性的值。
4.最后进入我们最核心的脚本SF_Evaluator.cs。
passType是定义pass块类型的,这里大家可以创建一个OutlineEqual字段。我这里没这么做,因为两种描边只是在顶点和片元函数中的计算处理中不同,其他地方都一样,我这里定义了一个bool值,用来判断是否是等宽屏幕的描边。
下面就行进行自动生成shader代码的处理了。首先在OutlinePass函数中加上isOutlineEqualWidth=false。说明这里是非等宽描边的处理。
然后把这个函数复制出来一个,名字改成OutlineEqualPass。在这个函数的基础上去改变一些代码。
首先加上isOutlineEqualWidth =ture。下面就是生成代码的函数了,我们挨个去修改。
找到Fragment()片元函数。修改两处。
在2593行添加一个分支判断,如果是等宽描边生成的片元函数头部取得的是 SV_Target 。
else if (currentPass == PassType.Outline&&isOutlineEqualWidth==true) { //添加一段等宽描边的判断
string vface = "";
if (dependencies.frag_facing)
{
vface = ", float facing : VFACE";
}
App("float4 frag(VertexOutput i" + vface + ") : SV_Target {");
}
2664行非等宽描边加上&&isOutlineEqualWidth==false判断,然后在加上等宽描边的分支判断。
else if( currentPass == PassType.Outline&&isOutlineEqualWidth==false ) {
App( "return fixed4(" + ps.n_outlineColor + ",0);" );
}
///程广英自定义的代码
else if (currentPass == PassType.Outline && isOutlineEqualWidth == true)
{
App("return float4(" + ps.n_outlineColor + ",1);");
}
片元函数修改完了,接下来修改顶点函数的代码处理就行了。找到Vertex()函数。
2466行的 if( currentPass == PassType.Outline &&isOutlineEqualWidth==false) 这行代码添加一个&&isOutlineEqualWidth==false判断。
然后添加一个if分支进行等宽描边的计算处理。如上图,这些代码不在细讲。APP()函数里面的内容就是后面连线到outlineEqualWidth后自动生成的代码段。等宽描边的计算原理主要就是这些公司,n_outlineEqualWidth这个字段的声明之前已经做过了,我这边连线的是slider节点,slider的值会传到这里进行计算。
至此,所有工作已经完成,当利用shaderforge进行可视化编辑时,如果有描边处理,连接到了outlineEqualWidth后,会自动生成相关pass块,如下代码:
Pass {
Name "Outline"
Tags {
}
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_shadowcaster
#pragma multi_compile_fog
#pragma only_renderers d3d9 d3d11 glcore gles gles3
#pragma target 3.0
uniform float _Outline;
uniform float4 _OutlineColor;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
float4 pos : SV_POSITION;
UNITY_FOG_COORDS(0)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
float4 pos =mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.5;
float dist = distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, v.vertex));
pos = pos + float4(normalize(normal), 0) * _Outline * 0.01 * dist;
o.pos = mul(UNITY_MATRIX_P, pos);
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
float4 frag(VertexOutput i) : SV_Target {
return float4(_OutlineColor.rgb,1);
}
ENDCG
}
是不是跟之前我们方案一里面通过修改shader代码里面的代码一样呢?
四、生成dll文件
源代码修改好后,在visual studio中创建一个C#类库(名字叫ShaderForge),添加两个引用,unityEditor.dll和unityEngine.dll
这两个dll在你的unity文件夹里自己找,都有的。
然后把code(修改好的源代码)文件夹导入进来,生成解决方案即可生成shaderforge.dll。然后把这个dll替换掉之前unity工程里的shaderforge.dll。如果有源代码的话源代码可以删除掉了,不然会有代码冲突。
注意:
1.生成时,目标框架更改为.Net Framework3.5及以下,高版本的dll导入到unity2018不支持。
2.如果生成时报错(public修饰符不可用、XX方法已过时等等)。定位到这些错误,如果有if Unity_2018 #else #else if字样。保留#ifUNITY_2018 #else之间的代码。把#else #endif之间的代码删掉,说明其中的代码中一些方法在unity2018里已经过时不可用。
至此,shaderforge的编辑器扩展及源代码修改的工作完成。