|
第一步 模型与贴图分析
从3ds max导入角色模型和动画到Unity
- 打开模型,旋转模型的轴向与unity世界空间坐标系下的轴向一致。选择编辑器列表的材质和UV展开,查看模型材质的个数和UV。导入动画。
- 导出白模和骨骼动画文件,设置导出的单位(unit)为厘米(Unity中的标准单位),分开导出便于管理。
- 导入Unity,检查模型和动画是否有问题,关闭动画压缩(Anim Compression),设置合适的动画状态机。
贴图
2、由于我使用的模型贴图是从UE4里抓包的,UE4和Unity使用的坐标系不一样,所以要进行一次法线贴图绿色通道反相就可以得到正确的法线纹理。法线纹理使用模型顶点的切线空间来存储模型,它的作用是修改模型的法线,为模型提供更多的细节。
3、第三张贴图是一张Mask贴图,它的作用是让我们可以单独处理某些细节部位,如服装上的金属。它的使用方式是采样Mask贴图上的像素值,再使用其中的通道值和表面属性相乘,当通道的值为0时,该属性不受影响。在本项目中分别通过Mask贴图的r和g通道控制模型的粗糙度和金属度。
有了角色模型和贴图信息就可以构建渲染框架了,首先来分析设定图,人物全身主要包含三部分材质,皮肤、布料、金属和头发。皮肤和布料渲染属于直接光漫反射,金属和头发属于直接光镜面反射。
第二步 光照框架
Lambert漫反射光照
在世界坐标系下计算法线和Directional Light漫反射。过程中需将法线贴图进行采样得到切线空间下的法线方向,并计算出世界坐标系下模型的TBN矩阵(T:tangent_world B:binormal_world N:normal_world),TBN矩阵与切线空间下的法线向量相乘就可以得到世界空间下的法线。计算Lambert漫反射只需再乘灯光方向。
Shader "Unlit/CharlotteBodyShader"
{
Properties
{
_BaseMap ("Base Map", 2D) = "white" {}
_NormalMap("Normal Map",2D) = "bump"{}
_CompMask("Comp Mask(RM)",2D) = "white"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 normal_world : TEXCOORD1;
float3 pos_world : TEXCOORD2;
float3 tangent_world : TEXCOORD3;
float3 binormal_world : TEXCOORD4;
LIGHTING_COORDS(5, 6)
};
sampler2D _BaseMap;
sampler2D _NormalMap;
sampler2D _CompMask;
float4 _LightColor0;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
// Dir
o.normal_world = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz);
o.tangent_world = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
o.binormal_world = normalize(cross(o.normal_world,o.tangent_world)) * v.tangent.w;
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Texture Info
half4 base_color_gamma = tex2D(_BaseMap, i.uv);
half4 base_color = pow(base_color_gamma, 2.2);
half4 comp_mask = tex2D(_CompMask, i.uv);
half4 normal_map = tex2D(_NormalMap, i.uv);
half3 normal_data = UnpackNormal(normal_map);
// Dir
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
half3 normal_world = normalize(i.normal_world);
half3 tangent_world = normalize(i.tangent_world);
half3 binormal_world = normalize(i.binormal_world);
float3x3 TBN = float3x3(tangent_world, binormal_world, normal_world);
normal_world = normalize(mul(normal_data.xyz, TBN));
// Light Info
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
half atten = LIGHT_ATTENUATION(i);
// Direct Diffuse直接光漫反射
half diff_term = max(0.0,dot(normal_world, light_dir));
half3 direct_diffuse = diff_term * _LightColor0.xyz * base_color.xyz * atten;
return float4(direct_diffuse, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
直接光漫反射(Lambert)
Blinn-Phong高光反射
使用Blinn-Phong 反射光模型计算角色的高光,需要计算三种光照类型。
- 「高光」:人眼观察角度和光线方向呈法线镜面对称时,可以看到
- 「漫反射光」:物体朝着任意方向反射相同亮度的光照到人眼
- 「环境光」:物体表面完全背对光源,但其它物体反射光照照射到该物体表面上,再通过该物体反射光照到人眼
需要绘制高光颜色的区域在_CompMask贴图中0.5-1.0部分,也就是灰色和白色区域,黑色区域为皮肤不在此绘制高光,对应的代码为half3 spec_color =lerp(0.04, albedo_color.rgb, metal);
lerp函数的作用相当于PS里的蒙版,metal部分显示高光颜色,其余部分为黑色。half smoothness =1.0- roughness;这段代码确定光滑度的大小,也就是确定高光区域的大小。
// Texture Info
half4 base_color_gamma = tex2D(_BaseMap, i.uv); // gamma空间下的颜色值,不是线性的
half4 albedo_color = pow(base_color_gamma, 2.2); // 正常线性颜色值
half4 comp_mask = tex2D(_CompMask, i.uv);
half roughness = comp_mask.r;
half metal = comp_mask.g;
half3 base_color = albedo_color.rgb * (1 - metal); // 固有色
half3 spec_color = lerp(0.04, albedo_color.rgb, metal); //高光颜色
half4 normal_map = tex2D(_NormalMap, i.uv);
half3 normal_data = UnpackNormal(normal_map);
// Direct Specular直接光镜面反射
half3 half_dir = normalize(light_dir + view_dir);
half NdotH = dot(normal_world, half_dir);
half smoothness = 1.0 - roughness;
half shininess = lerp(1, _SpecShiness, smoothness);
half spec_term = pow(max(0.0, NdotH), shininess * smoothness);
half3 direct_specular = spec_term * spec_color * _LightColor0.xyz * atten;
直接光镜面反射(Blin-Phone)
直接光漫反射(Lambert) + 直接光镜面反射(Blin-Phone)
SH间接光漫反射
在上面我们计算了直接光漫反射,但这不能满足我们,因为光并不是那么简单,光会被反射,会被折射,会被透射,会被吸收,所以物体的受光情况同时又由这个场景的其他物体决定,这部分光照同时拥有着更加富强的表现力,被我们称作间接光。由于间接光的复杂性,我们不能用几个光照求和来计算,因此经常使用Cubemap处理间接光,如IBL技术。IBL需要对CubeMap对应各个方向的光照进行采样,这样消耗巨大,SH就可以解决这个问题。
球谐光照实际上就是把环境光采样简化的几个系数,渲染时用这个系数就可以还原环境光照。
球谐函数计算
float3 custom_sh(float3 normal_dir)
{
float4 normalForSH = float4(normal_dir, 1.0);
//SHEvalLinearL0L1
half3 x;
x.r = dot(custom_SHAr, normalForSH);
x.g = dot(custom_SHAg, normalForSH);
x.b = dot(custom_SHAb, normalForSH);
//SHEvalLinearL2
half3 x1, x2;
// 4 of the quadratic (L2) polynomials
half4 vB = normalForSH.xyzz * normalForSH.yzzx;
x1.r = dot(custom_SHBr, vB);
x1.g = dot(custom_SHBg, vB);
x1.b = dot(custom_SHBb, vB);
// Final (5th) quadratic (L2) polynomial
half vC = normalForSH.x*normalForSH.x - normalForSH.y*normalForSH.y;
x2 = custom_SHC.rgb * vC;
float3 sh = max(float3(0.0, 0.0, 0.0), (x + x1 + x2));
sh = pow(sh, 1.0 / 2.2);
return sh;
}
// Indirect Diffuse 间接光的漫反射
half half_lambert = ( diff_term + 1.0 ) * 0.5;
float3 env_diffuse = custom_sh(normal_world) * base_color * half_lambert;球谐系数的参数表示
[HideInInspector]custom_SHAr("Custom SHAr", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHAg("Custom SHAg", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHAb("Custom SHAb", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHBr("Custom SHBr", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHBg("Custom SHBg", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHBb("Custom SHBb", Vector) = (0, 0, 0, 0)
[HideInInspector]custom_SHC("Custom SHC", Vector) = (0, 0, 0, 1)拖入CubeMap,用工具生成SH系数。拖入Obj,生成下图所示的球谐光照的结果。
间接光漫反射(SH)_没加base_color
间接光漫反射(SH)
SH的加入让人物阴影部分提亮,阴影过渡更柔和
直接光漫反射(Lambert) + 直接光镜面反射(Blin-Phone) + 间接光漫反射(SH)
IBL间接光镜面反射
texCUBElod()是一个Unity内置函数,用于采样CUBE贴图,且可以控制贴图的mipmap粗糙度功能,在Unity里使用可以使用自带的IBL技术只需将CUBE的convolution type设置为specular unity会自动生成9张mipmap,我们再通过texCUBElod()函数控制(lod就是level of detail)。
使用Mask贴图的r通道里的粗糙度信息控制mipmap的范围。
// Texture Info
half4 albedo_color_gamma = tex2D(_BaseMap, i.uv); // gamma空间下的颜色值,不是线性的
half4 albedo_color = pow(albedo_color_gamma, 2.2); // 正常线性颜色值
half4 comp_mask = tex2D(_CompMask, i.uv);
half roughness = saturate(comp_mask.r + _RoughnessAdjust);
half metal = saturate(comp_mask.g + _MetalAdjust);
half3 base_color = albedo_color.rgb * (1 - metal); // 固有色
half3 spec_color = lerp(0.0, albedo_color.rgb, metal); //高光颜色
// Indirect Specular 间接光的镜面反射
roughness = roughness * (1.7 - 0.7 * roughness);
float mip_level = roughness * 9.0;
half3 reflect_dir = reflect(-view_dir, normal_world);
half4 color_cubemap = texCUBElod(_EnvMap, float4(reflect_dir, mip_level));
half3 env_color = DecodeHDR(color_cubemap, _EnvMap_HDR);//确保在移动端能拿到HDR信息
#ifdef _IBLCHECK_ON
half3 env_specular = env_color * _Expose * spec_color * half_lambert;
#else
half3 env_specular = half3(0.0, 0.0, 0.0);
#endif
IBL的加入让金属高光更明显
直接光漫反射(Lambert) + 直接光镜面反射(Blin-Phone) + 间接光漫反射(SH) + 间接光镜面反射(IBL)
Tonemapping色调映射
本质上来讲,色调映射是要解决的问题是进行大幅度的对比度衰减将场景亮度变换到可以显示的范围,同时要保持图像细节与颜色等对于表现原始场景非常重要的讯息。
根据应用的不同,色调映射的目标可以有不同的表述。在有些场合,生成“好看”的图像是主要目的,而在其它一些场合可能会强调生成尽可能多的细节或者最大的图像对比度。在实际的渲染应用中可能是要在真实场景与显示图像中达到匹配,尽管显示设备可能并不能够显示整个的亮度范围。
inline float3 ACES_Tonemapping(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
float3 encode_color = saturate((x*(a*x + b)) / (x*(c*x + d) + e));
return encode_color;
};
final_color = ACES_Tonemapping(final_color);Tonemapping的加入让颜色细节更多,色彩对比度变强
SSS(次表面散射)皮肤
皮肤是一种特殊的介质,受到光照的时候,会有透光的现象,表现为皮肤光照区域上的一段高饱和度的渐变色。
我们预先生成一张SSS皮肤贴图,再进行采样。
// Direct Diffuse直接光漫反射
half diff_term = max(0.0, dot(normal_world, light_dir));
half3 common_diffuse = diff_term * base_color * atten * _LightColor0.xyz;
half2 uv_lut = half2(diff_term * atten + _SSSOffset, _CurveOffset);
half3 lut_color_gamma = tex2D(_SkinLUT, uv_lut);
half3 lut_color = pow(lut_color_gamma, 2.2);
half3 sss_diffuse = lut_color * base_color * _LightColor0.xyz;
#ifdef _DIFFUSECHECK_ON
half3 direct_diffuse = lerp(common_diffuse, sss_diffuse, skin_area);
#else
half3 direct_diffuse = half3(0.0, 0.0, 0.0);
#endif
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|