怼 Real-Time Rendering 的第7天,写一篇NPR缓一缓,这几天就是看书,然后忙里偷闲玩几把Gwent,听说移动端快出了,舒服
上一篇关于卡通渲染的文章实现了一些卡通渲染基础操作 梯度漫反射、顶点扩张描边、边缘光、各向异性高光;
本篇继续对卡通渲染基本操作进行补充。
实验素材来自Toony Colors Pro
diffuse的经验模型一般都包含,
float3 ndl = max(0,dot(worldNormal, worldLightDir));
我们可以通过一个量来控制光影的比例
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
“划分”最常用的方法就是 先乘上一个数,再取整
float level = round(diff * _ToonSteps) / _ToonSteps;
这里 / _ToonSteps 目的是 在 0 - 1 范围内梯度
float interval = 1 / _ToonSteps;
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
因为smoothstep本身是平滑差值,当RampSmooth = 1,应该是完全平滑,就是说线性,所以这里可以用step或者if处理一下,建议step
Toony Colors Pro 2 中的做法是用 “_ShadowColor” 和 “_HightLIghtColor” 差值,把“_ShadowColor”的a通道作为差值系数,控制叠加在片段上的高光和阴影颜色
_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
......
float3 diffuse = albedo * lightColor * rampColor;
虽然需要额外的采样,但是可以增加阴影的层次感,对于不同部分添加不同的阴影颜色,美术更容易控制,不用调半天材质
float4 shadowTex = tex2D(_ShadowTex,i.uv);
albedo = lerp(shadowTex.rgb,albedo.rgb,ramp);
日式卡渲趋向于基于几何体生成的方法去描边,角色不同部位的描边粗细和颜色往往更容易控制
比如上图中 头发的描边和其他部位描边颜色, 衣服描边宽度的粗细变化,甚至有的地方出现了“断线”。
可以通过额外的纹理控制,充分利用各个通道,NPR没有统一标准,这里举一个例子:
A通道,控制描边的粗细,比如 0.0,就能做出断线的效果
RGB,控制描边颜色
还可以更复杂,比如:
A通道,描边的可见性
R通道,控制 顶点 到 摄像机 的扩张系数,说白了就是根据距离远近控制(乘)描边粗细
G通道,描边的粗细
B通道,可以预留,也可用来做阴影的遮罩,我们一般需要面部或其它一些部位颜色亮一些
曝光是提升视觉效果的常用手段
比如说,我们可以设置边缘光设置更高的强度,根据需求控制人物不同位置的曝光程度。
基本和梯度漫反射一样,可以提供对边缘光面积和平滑程度的控制。原理也基本同上
增加额外的控制高光的灰度图或通道,或者各向异性高光,在上一篇文章已经提到
以前向渲染为前提
除了不同光源的衰减计算之外,在ForwardBase中计算过的东西,是完全搬运到ForwardAdd中,还是有选择性的去掉一些东西。 这取决于Shader的情况和需求。 NPR和BPR在这一方面是完全不同的,效果好看就完事儿了。
实验素材来自MiHoYo的崩坏,它的人物渲染和罪恶装备Xrd极为相似。
前几天还原了MiHoYo的一个人物,好像叫白练?,除了头发对于面部的阴影,其他地方大体还原了(头发对面部的阴影按理说也该有了,就是没效果,我以为是暗面计算有问题,可是换成两个片面又没问题了,目前没找出来错误在哪)
顺便提一下,很牛批的软件,就是目前导出图片的时候没Alpha,采样a全是1.0,导致我还得用PS自己加通道,我感觉有必要反馈一下。
MainTex: rgb 颜色, a 是否受光照影响
LightMap:r控制高光颜色, g 控制固定阴影 b 控制高光范围。这是一张RGB图,a没用
高光和阴影控制什么的其实上面也都有提到,就是利用通道控制,只不过用的Trick不一样罢了,很多博主都讲了,我就不再赘述了。
有3个Pass,ForwardBase,ForwardAdd,ShadowCaster。
卡通渲染不一定要OutLine,这个Shader只是用来看着玩,NPR务必根据需求自己 写 (借鉴+增删改)。
Shader "GaoShuai/MultiLightItem" {
Properties {
//颜色
_Color ("Color", Color) = (1, 1, 1, 1)
_HColor ("Highlight Color", Color) = (1.0, 1.0, 1.0, 1.0)
_SColor ("Shadow Color", Color) = (0.8, 0.8, 0.8, 1.0)
//主纹理
_MainTex ("Main Texture", 2D) = "white" {}
//梯度
_ToonSteps ("Steps of Toon", range(1, 9)) = 2
_RampThreshold ("Ramp Threshold", Range(0.1, 1)) = 0.5
_RampSmooth ("Ramp Smooth", Range(0, 1)) = 0.1
//高光
_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
_SpecSmooth ("Specular Smooth", Range(0, 1)) = 0.1
_Shininess ("Shininess", Range(0.001, 10)) = 0.2
_Gloss("Gloss",Range(0.0,1.0))=1.0
// 边缘光
_RimColor ("Rim Color", Color) = (0.8, 0.8, 0.8, 0.6)
_RimThreshold ("Rim Threshold", Range(0, 1)) = 0.5
_RimSmooth ("Rim Smooth", Range(0, 1)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile_fwdbase
#pragma multi_compile_instancing
#pragma enable_d3d11_debug_symbols
#include "Lighting.cginc"
#include "AutoLight.cginc"
float4 _Color;
float4 _HColor;
float4 _SColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _RampThreshold;
float _RampSmooth;
float _ToonSteps;
float _SpecSmooth;
float _Shininess;
float _Gloss;
float4 _RimColor;
float _RimThreshold;
float _RimSmooth;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
UNITY_FOG_COORDS(4)
};
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (a2v v) {
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
o.pos = UnityObjectToClipPos( v.vertex);
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
float4 frag(v2f i) : SV_Target {
float3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float4 c = tex2D (_MainTex, i.uv);
float3 albedo = c.rgb * _Color.rgb;
float Alpha = c.a*_Color.a;
float Specular = _Shininess;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
float3 floatDir = normalize(worldLightDir + worldViewDir);
float3 ndl = max(0,dot(worldNormal, worldLightDir)); //光照系数
float ndh = max(0, dot(worldNormal, floatDir)); //高光系数
float ndv = max(0, dot(worldNormal, worldViewDir)); //边缘光系数
//控制光影比例
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
float interval = 1 / _ToonSteps;
//简化颜色,划分色阶
float level = round(diff * _ToonSteps) / _ToonSteps;
float ramp ;
//平滑过渡色阶
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *= atten;
//使用颜色叠加,对高光部分和阴影部分叠色
_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
//高光计算
float spec = pow(ndh, Specular * 128.0) * _Gloss;
spec *= atten;
spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
//边缘光计算
float rim = (1.0 - ndv) * ndl;
rim *= atten;
rim = smoothstep(_RimThreshold - _RimSmooth * 0.5, _RimThreshold + _RimSmooth * 0.5, rim);
float4 color;
float3 diffuse = albedo * lightColor * rampColor; //叠加颜色
float3 specular = _SpecColor.rgb * lightColor * spec;
float3 rimColor = _RimColor.rgb * lightColor * _RimColor.a * rim;
color.rgb = diffuse + specular + rimColor;
color.a = Alpha;
UNITY_APPLY_FOG(i.fogCoord, color);
return color;
}
ENDCG
}
Pass {
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma multi_compile_instancing
#pragma enable_d3d11_debug_symbols
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
float4 _Color;
float4 _HColor;
float4 _SColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _RampThreshold;
float _RampSmooth;
float _ToonSteps;
float _SpecSmooth;
float _Shininess;
float _Gloss;
float4 _RimColor;
float _RimThreshold;
float _RimSmooth;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
};
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos( v.vertex);
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
float4 frag(v2f i) : SV_Target {
float3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float4 c = tex2D (_MainTex, i.uv);
float3 albedo = c.rgb * _Color.rgb;
float Alpha = c.a*_Color.a;
float Specular = _Shininess;
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif
float3 floatDir = normalize(worldLightDir + worldViewDir);
float3 ndl = max(0,dot(worldNormal, worldLightDir));
float ndh = max(0, dot(worldNormal, floatDir));
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
float interval = 1 / _ToonSteps;
float level = round(diff * _ToonSteps) / _ToonSteps;
float ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *=atten;
float spec = pow(ndh, Specular * 128.0) * _Gloss;
spec *= atten;
spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
float4 color;
float3 diffuse = albedo * lightColor * ramp;
float3 specular = _SpecColor.rgb * lightColor * spec;
color.rgb = specular + diffuse;
color.a = Alpha;
return color;
}
ENDCG
}
pass
{
Tags{ "LightMode" = "ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma enable_d3d11_debug_symbols
#include "UnityCG.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f o) :SV_Target
{
SHADOW_CASTER_FRAGMENT(o)
}
ENDCG
}
}
}