Unity——卡通渲染实现
效果展示:
原模型:
一、简单分析
卡通渲染又叫非真实渲染(None-Physical Rendering-NPR),一般日漫里的卡通风格有几个特点:
1.人物有描边
2.有明显的阴影分界线,没有太平滑的过渡
以下就根据这两点来实现卡渲效果;
二、描边
1.法线外扩
实现描边方式多种,比如卷积区分边界;
这里使用更简单的两个Pass,一个只用纯色画背面,利用法线外扩顶点,根据深度的不同这个纯色的背面会被显示出来,同时又不会遮挡正面;
Pass { Tags {"LightMode"="ForwardBase"} //裁剪正面,只画背面 Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" half _OutlineWidth; half4 _OutLineColor; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; float4 vertColor : COLOR; float4 tangent : TANGENT; }; struct v2f { float4 vertColor : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert (a2v v) { v2f o; UNITY_INITIALIZE_OUTPUT(v2f, o); //顶点沿着法线方向外扩 o.pos = UnityObjectToClipPos(float4(v.vertex.xyz + v.normal * _OutlineWidth * 0.1 ,1)); o.vertColor = fixed4(v.vertColor.rgb,1.0); return o; } half4 frag(v2f i) : SV_TARGET { return half4(_OutLineColor.rgb * i.vertColor.rgb, 0); } ENDCG }
2.细节处理(坑)
摄像机远近边缘线粗细不同
由于世界坐标系下做外扩,摄像机里物体远近会影响法线外扩的多少;
解决方案,在NDC坐标系下法线外扩;
//顶点着色器替换以下代码 float4 pos = UnityObjectToClipPos(v.vertex); //摄像机空间法线 float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz); //将法线变换到NDC空间,投影空间*W分量 float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w; //xy两方向外扩 pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a; o.pos = pos;
上下和左右边缘线粗细不同
NDC空间是正方形,而视口宽高比是长方体,导致描边上下和左右的粗细不统一;
解放方案,根据屏幕宽高比缩放法线再外扩;
//将近裁剪面右上角位置的顶点变换到观察空间 //unity_CameraInvProjection摄像机矩阵逆矩阵,UNITY_NEAR_CLIP_VALUE近截面值,DX:0,OpenGL-1.0;_ProjectionParams.y摄像机近截面 float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y)); //求得屏幕宽高比 float aspect = abs(nearUpperRight.y / nearUpperRight.x); ndcNormal.x *= aspect;
顶点重合法线不连续
模型顶点重合时会出现多条法线,在不同的面上法线不同导致描边不连续;
解决方案,修改模型顶点数据,同顶点多条法线求平均值;
需要和美工协商修改模型数据,这里写 3ff8 了脚本临时修改模型数据;
public class PlugTangentTools { [MenuItem("Tools/模型平均法线写入切线数据")] public static void WirteAverageNormalToTangentToos() { MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>(); foreach (var meshFilter in meshFilters) { Mesh mesh = meshFilter.sharedMesh; WirteAverageNormalToTangent(mesh); } SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(); foreach (var skinMeshRender in skinMeshRenders) { Mesh mesh = skinMeshRender.sharedMesh; WirteAverageNormalToTangent(mesh); } Debug.Log("重合顶点平均法线写入成功"); } private static void WirteAverageNormalToTangent(Mesh mesh) { var averageNormalHash = new Dictionary<Vector3, Vector3>(); for (var j = 0; j < mesh.vertexCount; j++) { if (!averageNormalHash.ContainsKey(mesh.vertices[j])) { averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]); } else { averageNormalHash[mesh.vertices[j]] = (averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized; } } var averageNormals = new Vector3[mesh.vertexCount]; for (var j = 0; j < mesh.vertexCount; j++) { averageNormals[j] = averageNormalHash[mesh.vertices[j]]; } var tangents = new Vector4[mesh.vertexCount]; for (var j = 0; j < mesh.vertexCount; j++) { tangents[j] = new Vector4(averageNormals[j].x, averageNormals[j].y, averageNormals[j].z, 0); } mesh.tangents = tangents; } }
细节处理前后对比:
ps:利用模型顶点的四个通道RGBA——对描边粗细显影相机距离缩放进行精细控制,需要美工配合;
三、着色
1.减少色阶
二分法
将有阴影和没阴影的地方做明显的区分;
half4 frag(v2f i) : SV_TARGET { half4 col = 1; half4 mainTex = tex2D(_MainTex, i.uv); half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); half3 worldNormal = normalize(i.worldNormal); half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //半兰伯特光照模型 half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5; //_ShadowRange区分阴影范围,_ShadowSmooth控制分界线的柔和程度,求出ramp值(百分比) half ramp = smoothstep(0, _ShadowSmooth, halfLambert - _ShadowRange); //根据ramp值插值取样,将阴影和main颜色混合 half3 diffuse = lerp(_ShadowColor, _MainColor, ramp); diffuse *= mainTex; col.rgb = _LightColor0 * diffuse; return col; }
Ramp贴图
使用明显分界的色阶图来取样,使阴影有明显的分界线;
逻辑和二分一样,只是多加个几个色阶;
//_ShadowRange范围取样Ramp贴图 half ramp = tex2D(_RampTex, float2(saturate(halfLambert - _ShadowRange), 0.5)).r;
高光色阶
卡渲高光和阴影一样,和周围色块有明显的分界线;
half3 specular = 0; half3 halfDir = normalize(worldLightDir + viewDir); half NdotH = max(0, dot(worldNormal, halfDir)); //_SpecularGloss控制高光光泽度 half SpecularSize = pow(NdotH, _SpecularGloss); //_SpecularRange高光范围,_SpecularMulti强度,在范围内显示高光有明显分界 if (SpecularSize >= 1 - _SpecularRange) { specular = _SpecularMulti * _SpecularColor; }
ilmTexture贴图
《GUILTY GEAR Xrd》中使用的方法,又叫Threshold贴图;
贴图的R通道控制漫反射的阴影阈值,G通道控制高光强度,B通道控制高光范围;
需要和美工配合,没贴图就不测了;
总之万物皆可用贴图来传递信息,rgba代表什么意思可以自行做各种trick;
half4 frag (v2f i) : SV_Target { half4 col = 0; half4 mainTex = tex2D (_MainTex, i.uv); //取样ilmTexture half4 ilmTex = tex2D (_IlmTex, i.uv); half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); half3 worldNormal = normalize(i.worldNormal); half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); //漫反射+阴影 half3 diffuse = 0; half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5; //g通道控制高光强度 half threshold = (halfLambert + ilmTex.r) * 0.5; half ramp = saturate(_ShadowRange - threshold); ramp = smoothstep(0, _ShadowSmooth, ramp); diffuse = lerp(_MainColor, _ShadowColor, ramp); diffuse *= mainTex.rgb; half3 specular = 0; half3 halfDir = normalize(worldLightDir + viewDir); half NdotH = max(0, dot(worldNormal, halfDir)); half SpecularSize = pow(NdotH, _SpecularGloss); //b通道控制高光遮罩 half specularMask = ilmTex.b; if (SpecularSize >= 1 - specularMask * _SpecularRange) { //g控制高光强度 specular = _SpecularMulti * (ilmTex.g) * _SpecularColor; } col.rgb = (diffuse + specular) * _LightColor0.rgb; return col; }
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(1)
【翻译】西川善司「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,前篇(2)
【翻译】西川善司的「实验做出的游戏图形」「GUILTY GEAR Xrd -SIGN-」中实现的「纯卡通动画的实时3D图形」的秘密,后篇
2.边缘泛光
三渲二加点边缘泛光会增加立体感,让画质更真实;效果如下;
_RimMin、_RimMax控制边缘泛光范围;
smoothstep使过渡平缓;再乘以RimColor,alpha控制强度;
half f = 1.0 - saturate(dot(viewDir, worldNormal)); half rim = smoothstep(_RimMin, _RimMax, f); rim = smoothstep(0, _RimSmooth, rim); half3 rimColor = rim * _RimColor.rgb * _RimColor.a; col.rgb = (diffuse + specular + rimColor) * _LightColor0.rgb;
3.mask遮罩图
用一张贴图来修正边缘泛光的效果;
边缘光的计算使用的是法线点乘视线。在物体的法线和视线垂直的时候,边缘光会很强。在球体上不会有问题,但是在一些有平面的物体,当平面和视线接近垂直的时候,会导致整个平面都有边缘光。这会让一些不该有边缘光的地方出现边缘光。
4.屏幕后效
post-processing官方组件中有bloom效果;
原理:提取图像中较亮区域,存储在纹理中,使用高斯模糊模拟光线扩散效果,将该纹理和原图像混合;过程比较复杂,后面写屏幕后期效果再分析吧;
完整Shader:
Shader "Unlit/CelRenderFull" { Properties { _MainTex ("MainTex", 2D) = "white" {} _IlmTex ("IlmTex", 2D) = "white" {} [Space(20)] _MainColor("Main Color", Color) = (1,1,1) _ShadowColor ("Shadow Color", Color) = (0.7, 0.7, 0.7) _ShadowSmooth("Shadow Smooth", Range(0, 0.03)) = 0.002 _ShadowRange ("Shadow Range", Range(0, 1)) = 0.6 [Space(20)] _SpecularColor("Specular Color", Color) = (1,1,1) _SpecularRange ("Specular Range", Range(0, 1)) = 0.9 _SpecularMulti ("Specular Multi", Range(0, 1)) = 0.4 _SpecularGloss("Sprecular Gloss", Range(0.001, 8)) = 4 [Space(20)] _OutlineWidth ("Outline Width", Range(0, 1)) = 0.24 _OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1) [Space(20)] _RimMin ("imMin",float) = 1.0 _RimMax ("RimMax",float) = 2.0 _RimSmooth("RimSmooth",Range(0.0,1))=0.5 _RimColor("RimColor",Color) = (1,1,1,1) } SubShader { Pass { Tags { "LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" sampler2D _MainTex; float4 _MainTex_ST; sampler2D _IlmTex; float4 _IlmTex_ST; half3 _MainColor; half3 _ShadowColor; half _ShadowSmooth; half _ShadowRange; half3 _SpecularColor; half _SpecularRange; half _SpecularMulti; half _SpecularGloss; half _RimMin; half _RimMax; half _RimSmooth; fixed4 _RimColor; struct a2v { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert (a2v v) { v2f o = (v2f)0; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } half4 frag (v2f i) : SV_Target { half4 col = 0; half4 mainTex = tex2D (_MainTex, i.uv); half4 ilmTex = tex2D (_IlmTex, i.uv); half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); half3 worldNormal = normalize(i.worldNormal); half3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); half3 diffuse = 0; half halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5; half threshold = (halfLambert + ilmTex.g) * 0.5; half ramp = saturate(_ShadowRange - threshold); ramp = smoothstep(0, _ShadowSmooth, ramp); diffuse = lerp(_MainColor, _ShadowColor, ramp); diffuse *= mainTex.rgb; half3 specular = 0; half3 halfDir = normalize(worldLightDir + viewDir); half NdotH = max(0, dot(worldNormal, halfDir)); half SpecularSize = pow(NdotH, _SpecularGloss); half specularMask = ilmTex.b; if (SpecularSize >= 1 - specularMask * _SpecularRange) { specular = _SpecularMulti * (ilmTex.r) * _SpecularColor; } half f = 1.0 - saturate(dot(viewDir, worldNormal)); half rim = smoothstep(_RimMin, _RimMax, f); rim = smoothstep(0, _RimSmooth, rim); half3 rimColor = rim * _RimColor.rgb * _RimColor.a; col.rgb = (diffuse + specular + rimColor) * _LightColor0.rgb; return col; } ENDCG } Pass { Tags {"LightMode"="ForwardBase"} Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" half _OutlineWidth; half4 _OutLineColor; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; float4 vertColor : COLOR; float4 tangent : TANGENT; }; struct v2f { float4 vertColor : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert (a2v v) { v2f o; UNITY_INITIALIZE_OUTPUT(v2f, o); float4 pos = UnityObjectToClipPos(v.vertex); float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.tangent.xyz); float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * pos.w; float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y)); float aspect = abs(nearUpperRight.y / nearUpperRight.x); ndcNormal.x *= aspect; pos.xy += 0.01 * _OutlineWidth * ndcNormal.xy * v.vertColor.a; o.pos = pos; o.vertColor = fixed4(v.vertColor.rgb,1.0); return o; } half4 frag(v2f i) : SV_TARGET { return half4(_OutLineColor.rgb * i.vertColor.rgb, 0); } ENDCG } } FallBack Off }
- 日式卡通渲染的效果的unity实现
- Unity中基于Gpu Instance进行大量物体渲染的实现与分析(一)
- Unity卡通渲染之描边处理
- 通过卡通渲染描边shader来学习Unity的Shader写法
- UnityEditor扩展编辑器实现从场景中渲染得到Cubemap
- 如何实现最佳的跨平台游戏体验?Unity成亮解密实时渲染
- 虚幻4修改引擎渲染管线,添加自定义ShadingModle实现卡通着色
- 一种简单实现卡通勾边渲染的方法
- unity实现切换渲染模式
- 如何实现最佳的跨平台游戏体验?Unity成亮解密实时渲染
- 【02】卡通渲染基本光照模型的实现
- 【游戏渲染】unity海边波浪效果的实现
- Unity场景渲染相关实现的猜想
- 【unity】 shader的渲染次序 实现,水只倒影 场景,不要倒影角色。
- unity中Toon(卡通)材质的下载与实现
- 《着色器和屏幕特效》读书笔记第八章-用Unity的渲染纹理实现屏幕特效
- [学习笔记][3dsMax][Maya][Shaderfx]卡通渲染Outline效果实现
- 【小松教你手游开发】【游戏渲染】unity海边波浪效果的实现
- Unity实现“笼中窥梦”的渲染效果
- Unity 3D 海水的实现2 折射与反射 离屏相机的渲染