假装是个前言
这是我在“基于GPU的绘制”这门课里上交的大程,作业要求是使用Shader来做一些东西。由于GLSL实在没学好,又被同学安利了Unity里的Shader,所以就尝试用Unity来完成了。最终做的效果十分……迷幻,不过成绩意外的还可以。
这是我真正意义上的第一次点开Unity,基本什么都不会。完全不懂C#脚本,只会像Maya一样手动移动和缩放。Shader也差不多是东抄一点西抄一点,努力往想要的效果上凑。不过作为一个入门作品,我觉得……勉强还是看得过去的!
在这里记录一些当时用到的东西,希望以后能进步起来 (•ㅂ•)/
想要的效果
作为一个数码宝贝脑残粉,在所有作业里想到夹带私货的第一反应当然就是……
想了想能用Shader比较好地展现出来的应该就是进化过程了:
(片源太久远,画质迷人)
来分析一下这里面可能能用Shader展示的大概有:
- 模型的旋转(不过这个我最后觉得用Shader太累赘,复制了个C#脚本解决了
- 模型的自发光(其实应该叫做辉光glow,但是最后我简化掉了改成了边缘发光
转念一想如果只上交这两个Shader,期末成绩肯定就凉了。必须得找点什么别的东西来充实一下,于是又想到了网侦里数码兽脚底下的那个阵:
(图片来自网络)
这里大概可以用Shader来实现魔法阵的流光效果和旋转效果。
再然后我又想到了之前小作业里写过的爆破Shader和网上有很多教程的消融Shader,也一并添加了进来。
魔改后的最终结果
这是我最终实现的迷之进化过程,动图太卡只好截图示意:
下面开始具体讲讲几个Shader的思路 (:з」∠)
突然安利
在Shader之前突然卖个安利!要夹带私货肯定得搞到要用的模型素材,在纠结半天要不要上淘宝直接买素材包以后我突然发现了这个神奇的网站,里面有各种游戏里提取出来的模型,简直不能更爽!
https://www.models-resource.com/
当然提取人家的模型不是什么好行为,不过非商用的话也就低调一点用用吧(
魔法阵的流光和旋转(片段着色器)
这部分用到的素材为一张png格式的魔法阵贴图(网上有很多资源)。
魔法阵的流光效果参考的是这里,讲得非常清楚:
http://blog.csdn.net/zzxiang1985/article/details/50706641
这里我遇到的问题是,照理说将雾效关掉以后,贴图的透明部分不应该有颜色,然而在我的程序中透明部分却显示了淡淡的白色。为了避开这个问题我干脆将天空盒换掉,使整个场景变成黑色。
旋转效果参考的是这里:
http://blog.csdn.net/u012322710/article/details/50858098
主要用到的即:
旋转矩阵的公式:
COS() - sin() , sin() + cos() 顺时针
COS() + sin() , sin() - cos() 逆时针
要注意把贴图的wrap Mode改为clamp,否则在旋转的时候四个角上会出现问题。
代码如下(VS会强行规范代码格式,十分鬼畜……)
Shader "Inception-Fx/FlowInsideOut" {
Properties{
_RotateSpeed("Rotate Speed",Range(0,10)) = 5
_MainTex("Base Texture (RGB)", 2D) = "white" {}
_MainColor("Base Color", Color) = (1, 1, 1, 1)
_FlowTex("Flow Texture (RGB)", 2D) = "black" {}
_FlowColorAtCenter("Flow Color At Center", Color) = (1, 1, 1, 1)
_FlowColorAtEdge("Flow Color At Edge", Color) = (1, 1, 1, 1)
_Period("Period (Seconds)", float) = 1
_FlowWidth("Flow Width", Range(0, 1)) = 0.1
_FlowHighlight("Flow Highlight", float) = 1
_AllAlpha("All Alpha", Range(0, 1)) = 1
}
SubShader{
Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }
Cull Off
Lighting Off
ZWrite Off
Fog{ Mode Off }
Blend One One
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _MainColor;
sampler2D _FlowTex;
fixed4 _FlowColorAtCenter;
fixed4 _FlowColorAtEdge;
float _Period;
float _FlowWidth;
float _FlowHighlight;
float _InvertDirection;
float _AllAlpha;
float _RotateSpeed;
struct appdata_t {
float4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : POSITION;
half2 mainTex : TEXCOORD0;
};
v2f vert(appdata_t v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.mainTex = v.texcoord;
return o;
}
fixed4 frag(v2f i) : COLOR
{
float2 mainTex = i.mainTex.xy - float2(0.5,0.5);
mainTex = float2(mainTex.x*cos(_RotateSpeed * _Time.y) - mainTex.y*sin(_RotateSpeed*_Time.y),
mainTex.x*sin(_RotateSpeed * _Time.y) + mainTex.y*cos(_RotateSpeed*_Time.y));
mainTex += float2(0.5,0.5);
fixed4 baseColor = tex2D(_MainTex, mainTex) * _MainColor;
float2 center = float2(0.5, 0.5);
float r = distance(mainTex, center);
float radiusMax = 0.5; // 从(0.5, 0.5)到(0.5, 1)的距离
float timeProgress = fmod(_Time.y, _Period) / _Period;
float flowRadiusMax = timeProgress * (radiusMax + _FlowWidth);
float flowRadiusMin = flowRadiusMax - _FlowWidth;
float isInFlow = step(flowRadiusMin, r) - step(flowRadiusMax, r);
float2 flowTexUV = float2((r - flowRadiusMin) / (flowRadiusMax - flowRadiusMin), 0);
fixed4 flowColor = _FlowColorAtCenter + timeProgress * (_FlowColorAtEdge - _FlowColorAtCenter);
fixed4 finalColor = baseColor + isInFlow * _FlowHighlight * tex2D(_FlowTex, flowTexUV) * flowColor * baseColor;
finalColor *= _AllAlpha;
return finalColor;
}
ENDCG
}
}
FallBack "Diffuse"
}
模型的爆破(几何着色器)
原理十分简单。只需在几何着色器中,将输入的三个顶点减少为输出的一个顶点即可。如图所示:
假设原模型上的三个点可以拼成一个左边这样的三角形,那么在几何着色器中,我们裁减掉三个顶点而增加它们中心的那个点。对于原模型上的所有顶点都执行这样的操作,模型就会显示为很多分散的颗粒。
要让这些颗粒运动起来,只需使用初中学过的加速度公式 S=vt+1/2at^2 即可。这里我们人为给定速度和加速度为10,方向为法线的方向。
效果如下:
Shader "Unlit/explosion"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Speed("Speed", Float) = 10
_AccelerationValue("AccelerationValue", Float) = 10
}
SubShader
{
Tags{ "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2g
{
float2 uv : TEXCOORD0;
float4 vertex : POSITION;
};
struct g2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _Speed;
float _AccelerationValue;
float _StartTime;
v2g vert(appdata v)
{
v2g o;
o.vertex = v.vertex;
o.uv = v.uv;
return o;
}
[maxvertexcount(1)]
void geom(triangle v2g IN[3], inout PointStream<g2f> pointStream)
{
if (_Time.y > 3) {
g2f o;
float3 v1 = IN[1].vertex - IN[0].vertex;
float3 v2 = IN[2].vertex - IN[0].vertex;
float3 norm = normalize(cross(v1, v2));
float3 tempPos = (IN[0].vertex + IN[1].vertex + IN[2].vertex) / 3;
float realTime = _Time.y - 3;
tempPos += norm * (_Speed * realTime + .5 * _AccelerationValue * pow(realTime, 2));
o.vertex = UnityObjectToClipPos(tempPos);
o.uv = (IN[0].uv + IN[1].uv + IN[2].uv) / 3;
pointStream.Append(o);
}
}
fixed4 frag(g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
模型边缘发光(片段着色器)
来看一下最终的效果:
这里看上去有一种迷之透明效果,实际上是投机取巧了,由于背景和主体颜色都是黑色,所以会让人产生一种视觉错觉 (:з」∠)
边缘描线的原理十分简单。对于每个顶点,如果abs(顶点法线方向 * 视线方向)< 阈值,就将其设定为描线的颜色即可。若要做出多层的效果,就缩小阈值,在边缘处将描线颜色改成白色(或者过度也行)。
Shader "Unlit/Outline"
{
Properties
{
_MainColor("【主颜色】Main Color", Color) = (0.5,0.5,0.5,1)
_TextureDiffuse("【漫反射纹理】Texture Diffuse", 2D) = "white" {}
_RimColor("【边缘发光颜色】Rim Color", Color) = (0.5,0.5,0.5,1)
_RimPower("【边缘发光强度】Rim Power", Range(0.0, 36)) = 0.1
_RimIntensity("【边缘发光强度系数】Rim Intensity", Range(0.0, 100)) = 3
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
}
Pass
{
Name "ForwardBase"
Tags
{
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#pragma target 3.0
uniform float4 _LightColor0;
uniform float4 _MainColor;
uniform sampler2D _TextureDiffuse;
uniform float4 _TextureDiffuse_ST;
uniform float4 _RimColor;
uniform float _RimPower;
uniform float _RimIntensity;
struct VertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct VertexOutput
{
float4 pos : SV_POSITION;
float4 texcoord : TEXCOORD0;
float3 normal : NORMAL;
float4 posWorld : TEXCOORD1;
LIGHTING_COORDS(3,4)
};
VertexOutput vert(VertexInput v)
{
VertexOutput o;
o.texcoord = v.texcoord;
o.normal = mul(float4(v.normal,0), unity_WorldToObject).xyz;
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag(VertexOutput i) : COLOR
{
float3 ViewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
float3 Normalection = normalize(i.normal);
float3 LightDirection = normalize(_WorldSpaceLightPos0.xyz);
float Attenuation = LIGHT_ATTENUATION(i);
float3 AttenColor = Attenuation * _LightColor0.xyz;
float NdotL = dot(Normalection, LightDirection);
float3 Diffuse = max(0.0, NdotL) * AttenColor + UNITY_LIGHTMODEL_AMBIENT.xyz;
half Rim = 1.0 - max(0, dot(i.normal, ViewDirection));
float3 Emissive = _RimColor.rgb * pow(Rim,_RimPower) *_RimIntensity;
float3 finalColor = Diffuse * (tex2D(_TextureDiffuse,TRANSFORM_TEX(i.texcoord.rg, _TextureDiffuse)).rgb*_MainColor.rgb) + Emissive;
return fixed4(finalColor,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
模型消融(片段着色器)
使用两张贴图,一张是正常的贴图,一张是噪声贴图。设定一个阈值,在噪声贴图上采样,如果噪声图上该点的某个值大于阈值,则将这个点discard掉。Discard的原理类似于return,不返回转换好的片元就终止处理了。阈值随时间减小即可出现动态效果。
如何产生过渡效果?
增加两个阈值,当采样点的值大于阈值A时,该点显示颜色A;当采样点的值大于阈值B时,该点显示颜色B;当采样点的颜色大于阈值C时,discard掉。
这段代码我应该是从哪里抄来的……关于这个效果网上有很多教程,随意参考即可。
Shader "Unlit/DissolveEffectX"
{
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_DissolveColorA("Dissolve Color A", Color) = (0,0,0,0)
_DissolveColorB("Dissolve Color B", Color) = (1,1,1,1)
_MainTex("Base 2D", 2D) = "white"{}
_DissolveMap("DissolveMap", 2D) = "white"{}
_DissolveThreshold("DissolveThreshold", Range(0,1)) = 1
_ColorFactorA("ColorFactorA", Range(0,1)) = 0.7
_ColorFactorB("ColorFactorB", Range(0,1)) = 0.8
}
CGINCLUDE
#include "Lighting.cginc"
uniform fixed4 _Diffuse;
uniform fixed4 _DissolveColorA;
uniform fixed4 _DissolveColorB;
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform sampler2D _DissolveMap;
uniform float _DissolveThreshold;
uniform float _ColorFactorA;
uniform float _ColorFactorB;
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float2 uv : TEXCOORD1;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
if(_Time.y>9)
_DissolveThreshold = 1 - 0.2*(_Time.y - 9);
//采样Dissolve Map
fixed4 dissolveValue = tex2D(_DissolveMap, i.uv);
//小于阈值的部分直接discard
if (dissolveValue.r < _DissolveThreshold)
{
discard;
}
//Diffuse + Ambient光照计算
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 lambert = saturate(dot(worldNormal, worldLightDir));
fixed3 albedo = lambert * _Diffuse.xyz * _LightColor0.xyz + UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 color = tex2D(_MainTex, i.uv).rgb * albedo;
//这里为了比较方便,直接用color和最终的边缘lerp了
float lerpValue = _DissolveThreshold / dissolveValue.r;
if (lerpValue > _ColorFactorA)
{
if (lerpValue > _ColorFactorB)
return _DissolveColorB;
return _DissolveColorA;
}
return fixed4(color, 1);
}
ENDCG
SubShader
{
Tags{ "RenderType" = "Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack "Diffuse"
}
总结
用Unity学习着色器确实比直接在OpenGL中写方便,因为不用写主程序,而且可以直接观察材质球来知道当前的效果。在做这个大程的过程中我在游戏特效网看了很多各种各样的特效,感觉非常神奇……稍微产生了将来当技术美术的冲动。希望自己能变强 (。・∀・)ノ゛