Cg Programming/Unity/Toon Shading卡通着色

2024-03-11 09:08

本文主要是介绍Cg Programming/Unity/Toon Shading卡通着色,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本教程涵盖了卡通着色(也叫做卡通渲染)作为非真实感渲染技术的一个实例。
这里写图片描述奶牛图标。本节中的所有图片都由Mariana Ruiz提供。
这是几个关于光照教程中的其中之一,这个光照超出了Phong反射模型的范围。但是,它是基于用章节“光滑镜面高光”中描述的Phong反射模型实现的逐像素光照。如果你还没有阅读过那章,建议你最好先读一下它。

非真实感渲染是计算机图形学中的一个非常宽泛的术语,它涵盖了所有的渲染技术,以及明显不同于实物照片外观的视觉风格。例子包括:孵化、概述、线性透视扭曲、粗抖动、粗颜色量化等。

卡通着色(也叫卡通渲染)是任何被用来实现一个卡通或手绘外观三维模型的非真实感绘制技术的子集。

一个指定视觉的着色器

这里写图片描述
皮克斯的John Lasseter曾在接受采访时说:“艺术挑战技术,技术激发艺术。”传统上用于描述三维对象的许多视觉风格和绘图技术实际上很难在着色器中实现。但是,我们根本没有理由不去尝试它。

当为指定视觉风格实现一个或多个着色器时,首先应该确定风格的特性哪些必须被实现。这是精确分析视觉风格例子的主要任务。没有这些例子,通常很难确认风格的特征。即使掌握某种风格的艺术家也往往无法恰当地描述这些特征;举个例子,因为他们不再意识到某些特征,或者认为某些特征是不必要的缺陷,不值得一提。

对于每一个特性,它应该被确定是否以及如何准确地实现它们。有些特性很容易实现,有些则很难通过程序员来实现或通过GPU来计算。因此,以John Lasseter的名言为精神的着色器程序员和(技术)艺术家之间的讨论通常是非常值得的,以决定哪些特性包括以及如何准确地重现它们。

非写实的镜面高光

这里写图片描述
跟在章节“光滑镜面高光”中实现的Phong反射模型相比较,本节图像中的镜面高光显然是白色的,没有任何其它颜色的添加。此外,它们还有一个非常锐化的边界。

通过计算Phong着色模型的高光项,以及设置片元颜色为镜面反射颜色乘以光源颜色(无衰减),如果镜面反射项大于某一阈值,比如最大强度的一半,我们可以实现这种程序式的镜面高光。

但是如果没有任何高光呢?通常,用户会为这种情况指定一个黑色镜面反射颜色;但是,用我们的方法会导致黑色的高光。解决这个问题的一个方法是考虑镜面反射的颜色,并且根据镜面颜色的不透明度将高光颜色和其它颜色“混合”。Alpha混合作为片元着色器的操作已经在章节“透明度”中描述过了。但是,如果所有的颜色都在片元着色器中已知,那么它也可以在片元着色器中计算。

在以下的代码片段中,假定fragmentColor已经分配了一个颜色,比如基于漫射照明。镜面颜色_SpecColor乘以光源颜色_LightColor0,然后在镜面颜色的不透明度_SpecColor.a上混合fragmentColor

 if (dot(normalDirection, lightDirection) > 0.0 // light source on the right side?&& attenuation *  pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), _Shininess) > 0.5) // more than half highlight intensity? {fragmentColor = _SpecColor.a * _LightColor0.rgb * _SpecColor.rgb+ (1.0 - _SpecColor.a) * fragmentColor;}

这就足够了吗?如果你仔细观察上图中公牛的眼睛,你将会看到一对镜面高光,即有一个以上的光源导致了镜面高光。在大多数教程中,我们通过有加性混合的第二个渲染通道把额外的光源考虑进来。但是,如果镜面高光的颜色不应该被添加到其它颜色上的话,加性混合就不应该被使用。相反,有镜面高光不透明颜色(通常)和其它片元的透明片元的的alpha混合将是一个可行的解决方案。(查看章节“透明度”中关于alpha混合的描述。)

非写实漫射照明

这里写图片描述
上面公牛图像中的漫射光照只包含了两种颜色:照亮毛皮的浅棕色和无光毛皮的深棕色。公牛其它部分的颜色与灯光无关。

实现这个的一种方法就是不管Phong反射模型的漫反射项是否达到了某个阈值都使用完整的漫反射颜色,比如大于0以及第二个颜色小于0。对于公牛的毛皮,这两种颜色将会是不一样的;对于其它部分,它们将会是一样的,这样在有光和无光区域就没有视觉差别了。对于阈值_DiffuseThreshold从深色_UnlitColor转换到浅色_Color(乘以光源颜色_LightColor0)的实现可能看起来像这样:

 float3 fragmentColor = _UnlitColor.rgb; 
if (attenuation * max(0.0, dot(normalDirection, lightDirection))>= _DiffuseThreshold)
{fragmentColor = _LightColor0.rgb * _Color.rgb; 
}

这就是上面图像中关于非写实照明的所有内容吗?仔细看一下就会发现在深棕色和浅棕色之间有一条浅的不规则的线。实际上,情况甚至会更复杂,深棕色有时并不包括上述技术所覆盖的区域,并且有时它包括了更多甚至超越了黑色轮廓。它在视觉风格上添加了丰富的细节并创建了手工绘制的外观。另一方面,在着色器中很难重现这一点。

轮廓

很多卡通着色器的特征之一是沿着模型的轮廓以一种指定的颜色勾勒(通常是黑色的,但也会是其它颜色,可以看看上图中的牛)。

在着色器中有不同的技术来达到这种效果。Unity 3.3在标准资源中附带了这样一个着色器,它通过在轮廓的颜色渲染放大模型的背面来呈现这些轮廓(通过移动表面法向量上的顶点坐标来放大),然后在上面绘制正面。这里我们用另一基于章节“轮廓增强”中技术:如果一个片元被确定为接近轮廓(原文为silhouette),它就被设置为轮廓线(原文为outline)的颜色。这个只对光滑表面有用,并且它会生成不同厚度的轮廓线(略厚或略薄取决于视觉风格)。但是,至少轮廓的总厚度应该由着色器的属性来控制。

这里写图片描述

我们做完了吗?如果你仔细看一下上图中的驴,你会看到在它腹部和耳朵的轮廓线比其它轮廓要厚得多。这样表示无光的区域;但是,厚度上的改变是连续的。模拟这种效果的一个方法是让用户指定两种总的轮廓线厚度:一个用于光照充足的区域,一个用作无光的区域(根据Phong反射模型中的漫反射项)。在这两个极端之间,厚度的参数应该被插值(也是根据Phong反射模型中的漫反射项)。但是,这样会使得轮廓线依赖于指定的光源;因此,以下的着色器只渲染第一个光源的轮廓线和漫射光照,这个光源通常是光源中最重要的一个。其它所有的光源只是渲染镜面高光。

以下的实现必须在_UnlitOutlineThickness(如果漫反射项的点乘结果小于等于0)和_LitOutlineThickness(如果点乘结果为1)之间进行插值。对于用0到1之间的参数x把值a线性变换到值b,Cg提供了内置函数lerp(a, b, x)。被插值的值随后会被用作阈值以决定一个点是否足够接近轮廓。如果它接近轮廓,它的片元颜色就被设置为轮廓线的颜色_OutlineColor

            if (dot(viewDirection, normalDirection) < lerp(_UnlitOutlineThickness, _LitOutlineThickness, max(0.0, dot(normalDirection, lightDirection)))){fragmentColor = _LightColor0.rgb * _OutlineColor.rgb; }

完整的着色器代码

目前为止应该很清楚的是,即使是上面的一些图像也会给忠实实现带来非常大的困难。因此,以下的着色器只实现了上面描述的一些特性而忽略了许多其它特性。注意不同颜色的贡献值(漫射照明、轮廓线、高光)是根据互相之间的遮挡来指定不同的优先级。你也可以把这些优先级考虑为不同的层放在彼此之上。

Shader "Cg shader for toon shading" {Properties {_Color ("Diffuse Color", Color) = (1,1,1,1) _UnlitColor ("Unlit Diffuse Color", Color) = (0.5,0.5,0.5,1) _DiffuseThreshold ("Threshold for Diffuse Colors", Range(0,1)) = 0.1 _OutlineColor ("Outline Color", Color) = (0,0,0,1)_LitOutlineThickness ("Lit Outline Thickness", Range(0,1)) = 0.1_UnlitOutlineThickness ("Unlit Outline Thickness", Range(0,1)) = 0.4_SpecColor ("Specular Color", Color) = (1,1,1,1) _Shininess ("Shininess", Float) = 10}SubShader {Pass {      Tags { "LightMode" = "ForwardBase" } // pass for ambient light and first light sourceCGPROGRAM#pragma vertex vert  #pragma fragment frag #include "UnityCG.cginc"uniform float4 _LightColor0; // color of light source (from "Lighting.cginc")// User-specified propertiesuniform float4 _Color; uniform float4 _UnlitColor;uniform float _DiffuseThreshold;uniform float4 _OutlineColor;uniform float _LitOutlineThickness;uniform float _UnlitOutlineThickness;uniform float4 _SpecColor; uniform float _Shininess;struct vertexInput {float4 vertex : POSITION;float3 normal : NORMAL;};struct vertexOutput {float4 pos : SV_POSITION;float4 posWorld : TEXCOORD0;float3 normalDir : TEXCOORD1;};vertexOutput vert(vertexInput input) {vertexOutput output;float4x4 modelMatrix = _Object2World;float4x4 modelMatrixInverse = _World2Object; output.posWorld = mul(modelMatrix, input.vertex);output.normalDir = normalize(mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);output.pos = mul(UNITY_MATRIX_MVP, input.vertex);return output;}float4 frag(vertexOutput input) : COLOR{float3 normalDirection = normalize(input.normalDir);float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);float3 lightDirection;float attenuation;if (0.0 == _WorldSpaceLightPos0.w) // directional light?{attenuation = 1.0; // no attenuationlightDirection = normalize(_WorldSpaceLightPos0.xyz);} else // point or spot light{float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;float distance = length(vertexToLightSource);attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource);}// default: unlit float3 fragmentColor = _UnlitColor.rgb; // low priority: diffuse illuminationif (attenuation * max(0.0, dot(normalDirection, lightDirection)) >= _DiffuseThreshold){fragmentColor = _LightColor0.rgb * _Color.rgb; }// higher priority: outlineif (dot(viewDirection, normalDirection) < lerp(_UnlitOutlineThickness, _LitOutlineThickness, max(0.0, dot(normalDirection, lightDirection)))){fragmentColor = _LightColor0.rgb * _OutlineColor.rgb; }// highest priority: highlightsif (dot(normalDirection, lightDirection) > 0.0 // light source on the right side?&& attenuation *  pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), _Shininess) > 0.5) // more than half highlight intensity? {fragmentColor = _SpecColor.a * _LightColor0.rgb * _SpecColor.rgb+ (1.0 - _SpecColor.a) * fragmentColor;}return float4(fragmentColor, 1.0);}ENDCG}Pass {      Tags { "LightMode" = "ForwardAdd" } // pass for additional light sourcesBlend SrcAlpha OneMinusSrcAlpha // blend specular highlights over framebufferCGPROGRAM#pragma vertex vert  #pragma fragment frag #include "UnityCG.cginc"uniform float4 _LightColor0; // color of light source (from "Lighting.cginc")// User-specified propertiesuniform float4 _Color; uniform float4 _UnlitColor;uniform float _DiffuseThreshold;uniform float4 _OutlineColor;uniform float _LitOutlineThickness;uniform float _UnlitOutlineThickness;uniform float4 _SpecColor; uniform float _Shininess;struct vertexInput {float4 vertex : POSITION;float3 normal : NORMAL;};struct vertexOutput {float4 pos : SV_POSITION;float4 posWorld : TEXCOORD0;float3 normalDir : TEXCOORD1;};vertexOutput vert(vertexInput input) {vertexOutput output;float4x4 modelMatrix = _Object2World;float4x4 modelMatrixInverse = _World2Object;output.posWorld = mul(modelMatrix, input.vertex);output.normalDir = normalize(mul(float4(input.normal, 0.0), modelMatrixInverse).rgb);output.pos = mul(UNITY_MATRIX_MVP, input.vertex);return output;}float4 frag(vertexOutput input) : COLOR{float3 normalDirection = normalize(input.normalDir);float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.rgb);float3 lightDirection;float attenuation;if (0.0 == _WorldSpaceLightPos0.w) // directional light?{attenuation = 1.0; // no attenuationlightDirection = normalize(_WorldSpaceLightPos0.xyz);} else // point or spot light{float3 vertexToLightSource = _WorldSpaceLightPos0.xyz - input.posWorld.xyz;float distance = length(vertexToLightSource);attenuation = 1.0 / distance; // linear attenuation lightDirection = normalize(vertexToLightSource);}float4 fragmentColor = float4(0.0, 0.0, 0.0, 0.0);if (dot(normalDirection, lightDirection) > 0.0 // light source on the right side?&& attenuation *  pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), _Shininess) > 0.5) // more than half highlight intensity? {fragmentColor = float4(_LightColor0.rgb, 1.0) * _SpecColor;}return fragmentColor;}ENDCG}} Fallback "Specular"
}

总结

恭喜,你完成了本章的学习。我们看到了:

  • 什么是卡通着色,卡通渲染,以及非真实感渲染。
  • 非真实感渲染中的一些技术是如何被使用到卡通着色中去的。
  • 如何在着色器中实现这些技术。

扩展阅读

  • 关于Phong反射模型和逐像素光照,你应该阅读章节“光滑镜面高光”。
  • 关于轮廓的计算,你应该阅读章节“轮廓增强”。
  • 关于混合,你应该阅读章节“透明度”。
  • 关于非真实感渲染技术,你应该阅读图书“OpenGL着色器语言”(第三版)中的第18章。

这篇关于Cg Programming/Unity/Toon Shading卡通着色的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/797361

相关文章

【Unity Shader】片段着色器(Fragment Shader)的概念及其使用方法

在Unity和图形编程中,片段着色器(Fragment Shader)是渲染管线中的一个阶段,负责计算屏幕上每个像素(片段)的颜色和特性。片段着色器通常在顶点着色器和任何几何处理之后运行,是决定最终像素颜色的关键步骤。 Fragment Shader的概念: 像素处理:片段着色器处理经过顶点着色器和几何着色器处理后,映射到屏幕空间的像素。颜色计算:它计算每个像素的颜色值,这可能包括纹理采样、光

【Unity Shader】Alpha Blend(Alpha混合)的概念及其使用示例

在Unity和图形编程中,Alpha Blend(也称为Alpha混合)是一种用于处理像素透明度的技术。它允许像素与背景像素融合,从而实现透明或半透明的效果。Alpha Blend在渲染具有透明度的物体(如窗户、玻璃、水、雾等)时非常重要。 Alpha Blend的概念: Alpha值:Alpha值是一个介于0(完全透明)和1(完全不透明)的数值,用于表示像素的透明度。混合模式:Alpha B

Apple - Media Playback Programming Guide

本文翻译整理自:Media Playback Programming Guide(Updated: 2018-01-16 https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/Introduction

Unity Meta Quest 开发:关闭 MR 应用的安全边界

社区链接: SpatialXR社区:完整课程、项目下载、项目孵化宣发、答疑、投融资、专属圈子 📕教程说明 这期教程我将介绍如何在应用中关闭 Quest 系统的安全边界。 视频讲解: https://www.bilibili.com/video/BV1Gm42157Zi 在 Unity 中导入 Meta XR SDK,进行环境配置后,打开 Assets > Plugins > An

Unity 字体创建时候容易导致字体文件不正确的一种情况

上面得到了两种字体格式,一种是TextMeshPro的,另一种是Unity UI系统中默认使用的字体资源。其原因是创建的位置不同导致的。 1.下面是TextMeshPro字体创建的位置 2:下面是Unity UI系统中默认使用的字体资源

摄像头画面显示于unity场景

🐾 个人主页 🐾 🪧阿松爱睡觉,横竖醒不来 🏅你可以不屠龙,但不能不磨剑🗡 目录 一、前言二、UI画面三、显示于场景四、结语 一、前言 由于标题限制,这篇文章主要是讲在unity中调用摄像头,然后将摄像头捕捉到的画面显示到场景中,无论是UI画面还是场景中的某个物体上;至于应用的场景可以用于AR增强现实。 那么话不多说,直接开始今

unity开发 --------- NGUI (UITable)

unity开发 --------- NGUI UITable与UIGrid相似,都是实现自动排序的。但UIGrid的元素大小是由我们来指定的,而Table中的元素的大小是根据元素本身计算出来的。 UITable还保存了元素的顺序List<Transform>。每次重排序,都会更新此List。除了要计算元素的Bound和保存List外,其他基本与UIGrid一致。 unity开发 ---

unity开发 --------- NGUI (UIGrid)

unity开发 --------- NGUI  UIGrid可以实现多个gameobject自动排序。可以设定其排序方向、每个元素的宽度,高度等。 public Arrangement arrangement = Arrangement.Horizontal;public int maxPerLine = 0;public float cellWi

unity开发 --------- NGUI(Localization、UILocalize)

unity开发 --------- NGUI NGUI支持动态加载资源功能。比如语言选择:假如当前语言为中文,当将语言更改为英文时,所有UI上的文字也立即变成了英文。此功能是用Localization和UILocalize两个脚本配合完成的。 Localization中记录多种配置方案,当更改配置方案时,由Localization发送通知,通知各UILocalize更新。 NGU

unity开发 --------- NGUI (UIViewPort、UIDraggableCamera)

unity开发 --------- NGUI 前面提到一种实现ScrollView的方法:unity开发 --------- NGUI (UIDragPanelContents、UIDraggablePanel、UICenterOnChild、UIScollBar、SpringPanel) 但上面那种发放有一个缺陷!它要用到shader。也就是说,对于低端设备,就不能以上面那种方式实现拖