在UNITY商店下了个免费的琥珀酱的model(好像叫UNITY CHAN,有兴趣的可以自己下载玩玩),发现作者已经写了几个shader,效果看起来还不错,不过不太完全,试着自己补一补
这是用标准着色器的效果,光照信息多了之后真是丑的不行……咱改改。不用bli-phong光照模型,用我们卡通逻辑的光照模型。
第一版shader
Shader "UnityChan-Self/Clohting" {
Properties {
_MainTex ("Diffuse", 2D)= "white" {} //albedo材质,定义底色
_FallOffSampler ("Falloff Control", 2D) = "white" {} //光照衰减取样
_FALLOFF_POWER ("Falloff Power", Float) = 0.3 //控制光照衰减取样强度
}
SubShader {
Tags {
"RenderType" = "Opaque"
"Queue" = "Geometry"
"LightMode" = "ForwardBase"
}
Pass {
Cull Back
ZTest LEqual
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _FallOffSampler;
float _FALLOFF_POWER;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 normal : TEXCOORD2;
float3 tangent : TEXCOORD3;
float3 binormal : TEXCOORD4;
float3 lightDir : TEXCOORD5;
};
v2f vert(appdata_tan v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
//_Object2World是针对四维向量的,如果要用法线直接乘,需要在后面补一个0,我们就直接用内置函数算了
//o.normal = mul(_Object2World, v.normal).xyz;
o.normal = UnityObjectToWorldNormal(v.normal);
half4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.viewDir.xyz = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz).xyz;
//得到世界空间视线方向
o.tangent = v.tangent.xyz;
o.binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
//副切线在切线空间里
o.lightDir = WorldSpaceLightDir(v.vertex);
return o;
}
float4 frag(v2f i) : SV_Target {
float4 diffuseSamplerColor = tex2D(_MainTex, i.uv.xy); //取材质色
float3 normalVec = i.normal;
float normalDotEye = dot(i.normal, i.viewDir.xyz); //视线和法线的点乘
float fallOffU = clamp(1.0 - abs(normalDotEye), 0.02, 0.98); //点乘取反
float4 fallOffSamplerColor = _FALLOFF_POWER * tex2D(_FallOffSampler, float2(fallOffU, 0.25f)); //取颜色衰减
float3 shadowColor = diffuseSamplerColor.rgb * diffuseSamplerColor.rgb; //将材质色平方,得到一个深色
float3 combinedColor = lerp(diffuseSamplerColor.rgb, shadowColor, fallOffSamplerColor.r); //根据颜色衰减,取材质色到深色的中间色
combinedColor *= (1.0 + fallOffSamplerColor.rgb * fallOffSamplerColor.a); //一定程度补偿深色变亮
return float4(combinedColor.rgb, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/Diffuse"
}
我们不用viewDir和lightDir的dot来计算光照效果,而是直接用viewDir和normal的点乘,这就有点像随时有个光源跟随着摄像机移动,减少光照的细节度,增加底层颜色的突出程度
直接把点乘结果输出的效果,有点像菲涅尔反射2333,不过是简化版本的。越白的地方意味着光照衰减取样越靠右
加上我们画好的光照衰减取样图,越左数值越靠近(0,0,0),越右越靠近(1,1,1),中间的渐变是日式卡通的特点,一般会有一个过渡色而不是硬过渡。说个题外话,实际上存储这个信息,只需要rgba四通道里的任意一个(我们实际上只需要一个0-1的float而不是现在的Vector3),所以可以往里面继续塞来存储其他信息
和底色混合后(0取原底色,1取原底色的平方),效果是这样的
其实讲究的日式卡通渲染应该是有两张底色的,一张作为“明”,一张作为“阴”,就是底色和阴影两张,不过我们没有,就直接把底色的开平方当做阴影色了。(有些更讲究的卡通渲染用的是3张底色图甚至4张)
感觉阴的颜色有点过了,用FALLOFF_POWER把阴的地方的数值稍微压一压,再加数值,做一定的亮度补偿,看自己感觉调整吧,毕竟NPR就是个 很主观的玄学玩意儿……
最后效果如图
然后做一个高光,依然是无视光源方向,把摄像机方向当做光源方向来运算
代码在fragment里接着上述代码
float4 reflectionMaskColor = tex2D(_SpecularReflectionSampler, i.uv.xy); //我也不太清楚这张高光图怎么来的
float specularDot = normalDotEye; //在真实系渲染里,这个值应该是Normal dot H向量(光照 + 视线的法向量,一般用H表示)
//这里把光照当做视线,所以H就是视线向量,Normal dot 视线向量就是上面求过的normalDotEye
float4 lighting = lit(normalDotEye, specularDot, _SpecularPower); //CG的内置函数
float3 specularColor = saturate(lighting.z) * reflectionMaskColor.rgb * diffuseSamplerColor.rgb;
combinedColor += specularColor; //add混合,只要是做加法,那肯定是变亮
lit函数的作用如下:
lit(NdotL, NdotH, m) (dot = 点乘) |
N表示法向量; L表示入射光向量; H表示半角向量; m表示高光系数。 函数计算环境光、散射光、镜面光的贡献,返回的4元向量。 X位表示环境光的贡献,总是1.0; Y位代表散射光的贡献,如果 NdotL<0,则为0;否则为NdotL Z位代表镜面光的贡献,如果NdotL<0 或者NdotH<0,则位0;否则为(NdotH)^m; W位始终位1.0 |
其实就是Blinn-Phong光照模型,不过内置了之后性能会高一点
模型的高光图是这样婶的
不是很懂怎么来的,感觉就是在一些边缘的地方加强了高光的反射,可能是指衣服的金属,塑料边缘之类的
最终效果(等等啊,为什么背心会这么反光啊,牛皮吗这是?)
原本作者还有一个环境反射的计算,不过我看了看,他是要做AR才需要写这个东西。我就不在这写了,直接到阴影投射
阴影投射其实是光衰减计算,这里考虑的就是世界空间的光源(不然还是像上面那样光源跟随摄像机还看个鬼的阴影)。很简单的计算,用内置函数就可以搞定了。我把整个代码再贴一次
第二版shader
Shader "UnityChan-Self/Clohting" {
Properties {
_MainTex ("Diffuse", 2D)= "white" {} //albedo材质,定义底色
_ShadowColor ("Shadow Color", Color ) = (0.8, 0.8, 1, 1)
_FallOffSampler ("Falloff Control", 2D) = "white" {} //光照衰减取样
_FALLOFF_POWER ("Falloff Power", Float) = 0.3 //控制光照衰减取样强度
_SpecularReflectionSampler ("Specular Control", 2D) = "white" {}
_SpecularPower ("Specular Power", Float) = 1
}
SubShader {
Tags {
"RenderType" = "Opaque"
"Queue" = "Geometry"
"LightMode" = "ForwardBase"
}
Pass {
Cull Off
ZTest LEqual
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _ShadowColor;
sampler2D _FallOffSampler;
float _FALLOFF_POWER;
sampler2D _SpecularReflectionSampler;
float _SpecularPower;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 normal : TEXCOORD2;
float3 tangent : TEXCOORD3;
float3 binormal : TEXCOORD4;
float3 lightDir : TEXCOORD5;
LIGHTING_COORDS( 6, 7 )
};
v2f vert(appdata_tan v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
//_Object2World是针对四维向量的,如果要用法线直接乘,需要在后面补一个0,我们就直接用内置函数算了
//o.normal = mul(_Object2World, v.normal).xyz;
o.normal = UnityObjectToWorldNormal(v.normal);
half4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.viewDir.xyz = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz).xyz;
//得到世界空间视线方向
o.tangent = v.tangent.xyz;
o.binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
//副切线在切线空间里
o.lightDir = WorldSpaceLightDir(v.vertex);
TRANSFER_VERTEX_TO_FRAGMENT( o );
return o;
}
float4 frag(v2f i) : SV_Target {
//Diffuse
float4 diffuseSamplerColor = tex2D(_MainTex, i.uv.xy); //取材质色
float3 normalVec = i.normal;
float normalDotEye = dot(i.normal, i.viewDir.xyz); //视线和法线的点乘
float fallOffU = clamp(1.0 - abs(normalDotEye), 0.02, 0.98); //点乘取反
float4 fallOffSamplerColor = _FALLOFF_POWER * tex2D(_FallOffSampler, float2(fallOffU, 0.25f)); //取颜色衰减
float3 shadowColor = diffuseSamplerColor.rgb * diffuseSamplerColor.rgb; //将材质色平方,得到一个深色
float3 combinedColor = lerp(diffuseSamplerColor.rgb, shadowColor, fallOffSamplerColor.r); //根据颜色衰减,取材质色到深色的中间色
combinedColor *= (1.0 + fallOffSamplerColor.rgb * fallOffSamplerColor.a); //一定程度补偿深色变亮
//Specular
float4 reflectionMaskColor = tex2D(_SpecularReflectionSampler, i.uv.xy); //我也不太清楚这张高光图怎么来的
float specularDot = normalDotEye; //在真实系渲染里,这个值应该是Normal dot H向量(光照 + 视线的法向量,一般用H表示)
//这里把光照当做视线,所以H就是视线向量,Normal dot 视线向量就是上面求过的normalDotEye
float4 lighting = lit(normalDotEye, specularDot, _SpecularPower); //CG的内置函数
float3 specularColor = saturate(lighting.z) * reflectionMaskColor.rgb * diffuseSamplerColor.rgb;
combinedColor += specularColor; //add混合,只要是做加法,那肯定是变亮
//EnviromentReflection
//因为我们不需要环境反射,所以就不写这个了
//Cast Shadow,接收阴影
shadowColor = _ShadowColor.rgb * combinedColor.rgb;
float attenuation = saturate(2.0 * LIGHT_ATTENUATION(i) - 1.0); //光衰减低的地方不处理,光衰减强的地方加强处理
combinedColor = lerp(shadowColor, combinedColor, attenuation);
return float4(combinedColor.rgb, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/Diffuse"
}
作者这里有个小trick,对光衰减进行了映射,光衰减弱的地方,不进行颜色混合。光衰减强的地方(就是光源照不到的地方),加强阴影处理。下面是对比图,上图进行了映射,下图没有,注意看腋下
最后是边缘高光
//Rimlight 边缘高光
float rimlightDot = saturate(0.5 * (dot(normalVec ,i.lightDir) + 1.0)); //把-1~1映射到0~1
fallOffU = saturate(rimlightDot * fallOffU); //先进行第一次映射,排除掉大部分非边缘部位
fallOffU = tex2D(_RimLightSampler, float2(fallOffU, 0.25f)).r; //第二次映射,再排除掉绝大部分非边缘部位
float3 lightColor = diffuseSamplerColor.rgb;
combinedColor += fallOffU * lightColor; //Add加亮
Add保证颜色变明亮
这里我不是太想使用菲尼尔的物理计算,那样细节太多,干脆用两张采样图映射两次,一样可以排除掉非边缘部位
第二张映射图
两次映射后的效果
只进行一次映射的效果
衣服这块的shader我们就打完了,下面是看skin的shader,其实就是cloth的shader删掉了Specular和环境反射。不过我发现好像改了代码之后cast shadow不能正常起效,建了个方块挡住平行光也看不到变化,不太知道为什么,如果有发现错误的大佬请留言给我,谢谢……
原diffuse太黄了,用FallOffPower稍微改白了一点,好看多了
眼睛也是直接用的skin.shader
skin代码:
Shader "UnityChan-Self/Skin" {
Properties {
_MainTex ("Diffuse", 2D)= "white" {} //albedo材质,定义底色
_ShadowColor ("Shadow Color", Color) = (0.8, 0.8, 1, 1)
_FallOffSampler ("FallOffSampler", 2D) = "white" {}
_FALLOFF_POWER ("Falloff Power", Float) = 1.0
_RimLightSampler ("RimLight Control", 2D) = "white" {}
}
SubShader {
Blend SrcAlpha OneMinusSrcAlpha, One One
Tags {
"RenderType" = "Overlay" //这个是他的工程有调用替换着色器,我们是删掉也无所谓
"IgnoreProjector"="True"
"Queue" = "Geometry"
"LightMode" = "ForwardBase"
}
Pass {
Cull Back
ZTest LEqual
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _ShadowColor;
sampler2D _FallOffSampler;
float _FALLOFF_POWER;
sampler2D _RimLightSampler;
struct v2f {
float4 pos : SV_POSITION;
LIGHTING_COORDS( 0, 1 )
float2 uv : TEXCOORD2;
float3 viewDir : TEXCOORD3;
float3 normal : TEXCOORD4;
float3 lightDir : TEXCOORD5;
};
v2f vert(appdata_tan v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
//_Object2World是针对四维向量的,如果要用法线直接乘,需要在后面补一个0,我们就直接用内置函数算了
//o.normal = mul(_Object2World, v.normal).xyz;
o.normal = UnityObjectToWorldNormal(v.normal);
half4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.viewDir.xyz = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz).xyz;
//得到世界空间视线方向
o.lightDir = WorldSpaceLightDir(v.vertex);
TRANSFER_VERTEX_TO_FRAGMENT( o );
return o;
}
float4 frag(v2f i) : Color {
//Diffuse
float4 diffuseSamplerColor = tex2D(_MainTex, i.uv.xy); //取材质色
float3 normalVec = i.normal;
float normalDotEye = dot(i.normal, i.viewDir.xyz); //视线和法线的点乘
float fallOffU = clamp(1.0 - abs(normalDotEye), 0.02, 0.98); //点乘取反
float4 fallOffSamplerColor = _FALLOFF_POWER * tex2D(_FallOffSampler, float2(fallOffU, 0.25f)); //取颜色衰减
float3 shadowColor = diffuseSamplerColor.rgb * diffuseSamplerColor.rgb; //将材质色平方,得到一个深色
float3 combinedColor = lerp(diffuseSamplerColor.rgb, shadowColor, fallOffSamplerColor.r); //根据颜色衰减,取材质色到深色的中间色
//EnviromentReflection
//因为我们不需要环境反射,所以就不写这个了
//Cast Shadow,接收阴影
shadowColor = _ShadowColor.rgb * combinedColor.rgb;
float attenuation = saturate(2.0 * LIGHT_ATTENUATION(i) - 1.0); //光衰减低的地方不处理,光衰减强的地方加强处理
combinedColor = lerp(shadowColor, combinedColor, attenuation);
//Rimlight 边缘高光
float rimlightDot = saturate(0.5 * (dot(normalVec ,i.lightDir) + 1.0)); //把-1~1映射到0~1
fallOffU = saturate(rimlightDot * fallOffU); //先进行第一次映射,排除掉大部分非边缘部位
fallOffU = tex2D(_RimLightSampler, float2(fallOffU, 0.25f)).r; //第二次映射,再排除掉绝大部分非边缘部位
float3 lightColor = diffuseSamplerColor.rgb *0.15; //数值比cloth的低,因为不想让皮肤有白色的边缘高光
combinedColor += fallOffU * lightColor; //Add加亮
//return float4(LIGHT_ATTENUATION(i), LIGHT_ATTENUATION(i), LIGHT_ATTENUATION(i), 1.0);
return float4(combinedColor.rgb, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/Diffuse"
}
最后的最后是描边效果,熟悉日漫的都知道,日漫都喜欢在线稿上描边。不过自动算的描边效果老实说很难达到近距离看所需要的要求,这时候就需要美工神出来施展神通了
效果图
描边的PASS放在渲染的Pass后面,用深度检测排除掉无需渲染的部分可以减少运算量。描边的颜色是根据diffuse加深得到的
Pass {
Cull Front
ZTest Less
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#define INV_EDGE_THICKNESS_DIVISOR 0.00285
#define SATURATION_FACTOR 0.6
#define BRIGHTNESS_FACTOR 0.8
sampler2D _MainTex;
float4 _MainTex_ST;
float _EdgeThickness;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.uv = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
half4 projSpacePos = UnityObjectToClipPos(v.vertex); //裁剪空间坐标
half4 projectSpaceNormal = normalize(UnityObjectToClipPos(half4(v.normal, 0))); //裁剪空间的法线
half4 scaleNormal = _EdgeThickness * INV_EDGE_THICKNESS_DIVISOR * projectSpaceNormal; //法线向外扩张
scaleNormal.z += 0.00001; //防止法线为0
o.pos = projSpacePos + scaleNormal; //坐标位移
return o;
}
float4 frag(v2f i) : SV_Target {
float4 diffuseColor = tex2D(_MainTex, i.uv);
float maxChan = max(max(diffuseColor.r, diffuseColor.g), diffuseColor.b); //rgb通道里存储最大的那个
float4 newMapColor = diffuseColor;
maxChan -= ( 1.0 / 255.0 );
float3 lerpVals = saturate( ( newMapColor.rgb - float3( maxChan, maxChan, maxChan ) ) * 255.0 ); //得到rgb最大的通道的1,其余为0
newMapColor.rgb = lerp( SATURATION_FACTOR * newMapColor.rgb, newMapColor.rgb, lerpVals ); //rgb最大通道的保留,其余降色
return float4( BRIGHTNESS_FACTOR * newMapColor.rgb * diffuseColor.rgb, diffuseColor.a );
}
ENDCG
}
EX补充
原作者没有给头发加头发独有的高光(什么叫头发独有的高光可以参考本文:http://www.graphics.stanford.edu/papers/hair/hair-sg03final.pdf),我觉得加了会更好看,所以试着加一下
上面文章的渲染效果太物理了,也太复杂了,不可能拿来实时渲染的,实时渲染这本书给了我们一个近似模型
设为当前点到光源方向,为当前点到视点方向,为头发的切线方向(从发根到末端),为头发的光泽度,则最终的高光强度系数可这样计算:
因为我们的模型不可能像真实头发那样细腻,而是一整块一整块的,所以需要一张燥波图来模拟头发副切线的随机偏移。并且实时渲染还指出,物理条件下同时存在主高光和副高光影响视觉效果,所以燥波图要两张。为了模拟主高光和副高光,对切线分别朝法线方向做两次不同程度的偏移,然后计算后相加:
因为我很懒,不想找第二张燥波图,所以只计算了写了主高光的情况
shader
//头发的反光,采用真实渲染方式
float3 mainTangent = i.binormal + normalVec * _OffSetRange * tex2D(_HairSpecularOffMap, i.uv.xy).r; //计算主高光副切线的偏移
float3 halfDir = normalize(i.viewDir + i.lightDir); //H向量
float dotTH = dot(mainTangent, halfDir);
float sqrTH =max(0.0001, sqrt(1 - pow(dotTH, 2)));
float atten =smoothstep(-1, 0, dotTH);
float primaryHairSpecularPower = atten * pow(sqrTH, _HairSpecularSmooth);
//return float4(primaryHairSpecularPower, primaryHairSpecularPower,primaryHairSpecularPower,1.0);
combinedColor += primaryHairSpecularPower * _HairSpecularRange * combinedColor; //直接取底色作为高光颜色,也可以自己另外弄
高光黑白效果图,看起来和现实情况已经比较像了
混合底色后还OK