在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