LowPoly
一种低多边形风格画作或效果。
参考文章:
【Unity Shader】新书封面 — Low Polygon风格的渲染
图形学进阶——曲面细分与几何着色器
效果如下:
原理
就我个人的理解来看,原始的法线如上面的左图,在片元着色器阶段,会将三角形三个法线做插值得到当前着色点的法线,从而产生平滑的效果,也就是右图。
而LowPoly风格正好是不需要平滑处理的,反而就是要左图这种棱角分明的感觉,所以核心思想就是当前所在三角形只使用一个法线,让该三角形区域的着色点使用的法线都是同一个。
Shader
Shader "NPR/LowPolyStyle"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Pass{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma target 4.0
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
struct v2g{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
struct g2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float3 faceNormal:TEXCOORD2;
};
v2g vert(appdata_base v){
v2g o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
[maxvertexcount(3)] //用于定义最大输出点
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream){
// 定义图元输入:point、 line、 lineadj、 triangle、 triangleadj 分别为点线面
// 定义图元输出:PointStream、 LineStream、 TriangleStream 同上
float3 A = IN[1].worldPos.xyz - IN[0].worldPos.xyz;
float3 B = IN[2].worldPos.xyz - IN[0].worldPos.xyz;
float3 fn = normalize(cross(A, B));
g2f o;
o.pos = IN[0].pos;
o.uv = IN[0].uv;
o.worldPos = IN[0].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[1].pos;
o.uv = IN[1].uv;
o.worldPos = IN[1].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[2].pos;
o.uv = IN[2].uv;
o.worldPos = IN[2].worldPos;
o.faceNormal = fn;
triStream.Append(o);
}
fixed4 frag(g2f i):SV_Target{
fixed3 lightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 normalDir = normalize(i.faceNormal);
fixed diff = saturate(dot(normalDir, lightDir));
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * diff;
return fixed4(diffuse + ambient, 1);
}
ENDCG
}
Pass {
Tags {"LightMode"="ForwardAdd"}
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma target 4.0
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
struct v2g {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float4 _ShadowCoord : TEXCOORD2;
};
struct g2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 faceNormal : TEXCOORD2;
float4 _ShadowCoord : TEXCOORD3;
};
v2g vert(appdata_base v) {
v2g o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o._ShadowCoord = mul(unity_WorldToShadow[0], mul(unity_ObjectToWorld, v.vertex));
return o;
}
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream) {
float3 A = IN[1].worldPos.xyz - IN[0].worldPos.xyz;
float3 B = IN[2].worldPos.xyz - IN[0].worldPos.xyz;
float3 fn = normalize(cross(A, B));
float3 worldPos = (IN[0].worldPos + IN[1].worldPos + IN[2].worldPos)/3.0;
g2f o;
o.pos = IN[0].pos;
o.uv = IN[0].uv;
o.worldPos = worldPos;
o.faceNormal = fn;
o._ShadowCoord = IN[0]._ShadowCoord;
triStream.Append(o);
o.pos = IN[1].pos;
o.uv = IN[1].uv;
o.worldPos = worldPos;
o.faceNormal = fn;
o._ShadowCoord = IN[1]._ShadowCoord;
triStream.Append(o);
o.pos = IN[2].pos;
o.uv = IN[2].uv;
o.worldPos = worldPos;
o.faceNormal = fn;
o._ShadowCoord = IN[2]._ShadowCoord;
triStream.Append(o);
}
fixed4 frag(g2f i) : SV_Target {
fixed3 lightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 normalDir = normalize(i.faceNormal);
fixed diff = saturate(dot(normalDir, lightDir));
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * diff * atten * atten;
return fixed4(diffuse, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
重点分析一下几何着色器的代码:
[maxvertexcount(3)] //用于定义最大输出点
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream){
// 定义图元输入:point、 line、 lineadj、 triangle、 triangleadj 分别为点线面
// 定义图元输出:PointStream、 LineStream、 TriangleStream 同上
float3 A = IN[1].worldPos.xyz - IN[0].worldPos.xyz;
float3 B = IN[2].worldPos.xyz - IN[0].worldPos.xyz;
float3 fn = normalize(cross(A, B));
g2f o;
o.pos = IN[0].pos;
o.uv = IN[0].uv;
o.worldPos = IN[0].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[1].pos;
o.uv = IN[1].uv;
o.worldPos = IN[1].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[2].pos;
o.uv = IN[2].uv;
o.worldPos = IN[2].worldPos;
o.faceNormal = fn;
triStream.Append(o);
}
其中 A, B 是用来获得三角面片的法线向量,如下图:
输入了三个顶点,输出三个顶点的法线值都为当前计算得到的这个,那么在传入到片元着色器之前所做的插值其实就并未改变法线向量,因为插值的三个法线向量都一样,怎么插值都是一样的。
得到法线后,后面的代码就和普通代码一样了,总体来说原理还是比较简单的,不过想要获得更有意思的效果,应该还有很多trick,学习之路无止境啊~~~