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;
顶点重合法线不连续
模型顶点重合时会出现多条法线,在不同的面上法线不同导致描边不连续;
解决方案,修改模型顶点数据,同顶点多条法线求平均值;
需要和美工协商修改模型数据,这里写了脚本临时修改模型数据;
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——卡通渲染实现的更多相关文章
- Unite 2018 | 《崩坏3》:在Unity中实现高品质的卡通渲染(上)
http://forum.china.unity3d.com/thread-32271-1-1.html 我们已经发布了Unite 2018 江毅冰的<发条乐师>.Hit-Point的&l ...
- Unite 2018 | 《崩坏3》:在Unity中实现高品质的卡通渲染(下)
http://forum.china.unity3d.com/thread-32273-1-1.html 今天我们继续分享米哈游技术总监贺甲在Unite Beijing 2018大会上的演讲<在 ...
- 基于Unity 5的次世代卡通渲染技术 -- Unite 2017 米哈游总监贺甲分享实录
在5月12日Unite2017开发者大会上,米哈游技术总监兼美术指导贺甲进行了主题为次世代卡通渲染的演讲.一下为详细分享内容: 大家好,首先自我介绍一下,我叫贺甲,在米哈游担任技术总监和美术指导工作, ...
- Unity Shader 卡通渲染 基于退化四边形的实时描边
从csdn转移过来,顺便把写过的文章改写一下转过来. 一.边缘检测算法 3D模型描边有两种方式,一种是基于图像,即在所有3D模型渲染完成一张图片后,对这张图片进行边缘检测,最后得出描边效果.一种是基于 ...
- Unity酱~ 卡通渲染技术分析(一)
前面的话 unitychan是日本unity官方团队提供的一个Demo,里面有很好的卡通渲染效果,值得参考学习 上图是我整理出来的shader结构,可以看到Unity娘被拆分成了很多个小的部件,我想主 ...
- Unity酱~ 卡通渲染技术分析(二)
前面的话 上一篇Unity酱~ 卡通渲染技术分析(一) 写了CharaMain.cginc,服装的渲染是怎么实现的.这篇来分析一下头发跟皮肤的实现 头发 本来以为unitychan的头发会有各向异性的 ...
- Unity Shader NPR 卡通渲染
卡通渲染的主要原理包含两个方面: 1.轮廓线的描边效果 2.模型漫反射离散和纯色高光区域的模拟 描边: 描边的实现方法采用将模型的轮廓线顶点向法线(或顶点)的方向扩展一定的像素得到.也可通过边缘检测( ...
- 【NPR】卡通渲染
写在前面 我的博客讲过好几篇卡通渲染了,比如[Unity Shader实战]卡通风格的Shader(一).[Unity Shader实战]卡通风格的Shader(二).[NPR]漫谈轮廓线的渲染.[S ...
- GGXX的卡通渲染实现 真的好变态......
最近在youtube上看了GDC,学了很多东西,最让我震撼的就是ggxx的卡通渲染了.感慨一下,想要用3D做出二次元的效果,真的不容易.现记录一些要点: 1)不要使用normal map来做cel-s ...
随机推荐
- Shell学习(三)——Shell条件控制和循环语句
参考博客: [1]Shell脚本的条件控制和循环语句 一.条件控制语句 1.if语句 1.1语法格式: if [ expression ] then Statement(s) to be execut ...
- [转]C++中const的使用
原文链接:http://www.cnblogs.com/xudong-bupt/p/3509567.html 平时在写C++代码的时候不怎么注重const的使用,长久以来就把const的用法忘记了 写 ...
- c学习 - 算法
简介: 一个程序包括两方面内容:数据结构.算法 数据结构:对数据的描述,包括数据的类型和数据的组织形式 算法:对操作的描述,即操作步骤 (程序=算法+数据结构) 算法是灵魂,数据结构是加工对象,语言是 ...
- OC Swift 走马灯效果
我们常见走马灯样式的功能,下面整理一下 Object-C 与 Swift 的实现代码 OC UILabel *label3 = [[UILabel alloc] initWithFrame:CGRec ...
- MySQL(2):数据管理
一. 外键概念: 如果公共关键字在一个关系中是主关键字,那么这个公共关键字被称为另一个关系的外键.由此可见,外键表示了两个关系之间的相关联系.以另一个关系的外键作主关键字的表被称为主表,具有此外键的表 ...
- Linux:expr、let、for、while、until、shift、if、case、break、continue、函数、select
1.expr计算整数变量值 格式 :expr arg 例子:计算(2+3)×4的值 1.分步计算,即先计算2+3,再对其和乘4 s=`expr 2 + 3` expr $s \* 4 2.一步完成计算 ...
- 【Linux】【Services】【Project】Haproxy Keepalived Postfix实现邮件网关Cluster
1. 简介: 1.1. 背景:公司使用exchange服务器作为邮件服务器,但是使用Postfix作为邮件网关实现病毒检测,内容过滤,反垃圾邮件等功能.原来的架构非常简单,只有两台机器,一个负责进公司 ...
- df和du显示不同
目录 一.简介 二.原因分析 三.解决方法 一.简介 Linux服务器,使用df -h查看文件系统使用率,可以看到/dev/xvdb1磁盘占用了约27G,挂载目录为/opt. 但进入到opt目录中执行 ...
- Python第三周 函数详解
def 函数名(): """注释说明"""" 执行逻辑体 return 返回值 定义不带参数的函数 带参数的函数 默认参数 这个是 ...
- 阿里巴巴分布式服务框架HSF
HSF称之为高速服务框架HSF(High-speed Service Framework),是在阿里巴巴广泛使用的分布式RPC服务框架. HSF连通不同的业务系统,解耦系统间的实现依赖.HSF从分布式 ...