Shader 轮廓线(描边)总结

您所在的位置:网站首页 线描怎么做 Shader 轮廓线(描边)总结

Shader 轮廓线(描边)总结

2024-06-30 17:15| 来源: 网络整理| 查看: 265

在《Real Time Rendering, third edition》一书中,作者把轮廓线的实现方法分成5种类型

基于观察角度和表面法线的轮廓线渲染过程式几何轮廓线渲染,使用两个Pass渲染基于图像处理的轮廓线渲染(屏幕后处理)基于轮廓边检测的轮廓线渲染混合了上述的几种渲染方法 基于观察角度和表面法线的轮廓线渲染

原理:法线和视线垂直的地方认为是边缘,这种方法和实现边缘光类似,可以参考这篇文章,Shader边缘光 优点:这种方法简单快速,可以在一个Pass中就得到渲染结果 缺点:局限性很大,轮廓效果很难控制,很多模型渲染出来的描边效果都不尽如人意

过程式几何轮廓线渲染,使用两个Pass渲染 顶点外扩

原理:第一个Pass渲染背面的面片,并使用某些技术让它的轮廓可见;第二个Pass再正常渲染正面的面片 优点:快速有效,并且适用于绝大多数表面平滑的模型 缺点:不适合表面法线不连续的模型,如立方体轮廓线会出现断裂的情况。 在这里插入图片描述 如图,第一个Pass使用轮廓线颜色渲染整个背面的面片,把模型顶点沿着法线方向向外扩张一段距离,第二个Pass渲染正面,shader实现如下

Shader "MyCustom/ToneBasedShading" { Properties { _MainColor ("[主颜色] Main Color", Color) = (1, 1, 1, 1) _MainTex ("[主纹理] Main Tex", 2D) = "white" {} _Ramp ("[漫反射色调的渐变纹理] Ramp Texture", 2D) = "white" {} _Outline ("[轮廓线宽度] Outline", Range(0, 1)) = 0.1 _OutlineColor ("[轮廓线颜色] Outline Color", Color) = (0, 0, 0, 1) _SpecularColor ("[高光的颜色] Specular Color", Color) = (1, 1, 1, 1) _SpecularScale ("[高光反射的阈值] Specular Scale", Range(0, 0.1)) = 0.01 } SubShader { Tags { "RenderType" = "Opaque" "Queue" = "Geometry"} //第一个Pass使用轮廓线颜色渲染整个背面的面片 Pass { //定义pass名称后,可以在其他shader里引用 NAME "Outline" //剔除正面,只渲染背面 Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; float _Outline; fixed4 _OutlineColor; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; //顶点和法线转到观察空间 o.vertex = mul(UNITY_MATRIX_MV, v.vertex); float3 normal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal)); //对z分量进行处理,防止内凹的模型扩张顶点后出现背面面片遮挡正面面片的情况 normal.z = -0.5; //在观察空间下把模型顶点沿着法线方向向外扩张一段距离 o.vertex = o.vertex + float4(normal, 0) * _Outline; //转换到裁剪空间 o.vertex = mul(UNITY_MATRIX_P, o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { //背面都使用轮廓线的颜色渲染 return float4(_OutlineColor.rgb, 1); } ENDCG } //第二个Pass渲染正面 Pass { Tags { "LightMode" = "ForwardBase" } Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #include "UnityShaderVariables.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; //阴影贴图放到TEXCOORD3 SHADOW_COORDS(3) }; fixed4 _MainColor; sampler2D _MainTex; sampler2D _Ramp; float4 _MainTex_ST; float4 _SpecularColor; fixed _SpecularScale; v2f vert (appdata 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; } fixed4 frag (v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); //半角向量 fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir); //反照率 fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb; //计算当前世界坐标下的阴影值,光照衰减和阴影值相乘后的结果存储到atten UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); //计算半兰伯特漫反射系数 fixed diff = dot(worldNormal, worldLightDir) * 0.5 + 0.5; //和阴影值相乘得到最终的漫反射系数 diff = diff * atten; //漫反射系数对渐变纹理_Ramp进行采样,和反照率光照颜色相乘,得到最后的漫反射光照 fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb; //卡通渲染中,高光是一块分界明显的纯色区域,所有不能用Blinn-Phong的公式去计算光照 fixed spec = dot(worldNormal, worldHalfDir); //和阈值进行比较,小于阈值则高光反射系数为0,否则为1,这种方法会在高光区域的边界造成锯齿 //原因是高光区域边缘是由0突变到1。为了处理抗锯齿,在边界处很小的一块区域内,进行平滑处理 //spec =step(threshold, spec); //邻域像素之间的近似导数值,w是一个很小的值,当spec-_SpecularScale小于-w时,返回0, //大于w时,返回1,否则在0到1之间进行插值。这样我们可以在[−w, w]区间内,即高光区域的边界处, //得到从0到1平滑变化的spec值 fixed w = fwidth(spec) * 2.0; //step(0.000 1, _SpecularScale),是为了_SpecularScale为0时,可以完全消除高光反射的光照 fixed3 specular = _SpecularColor.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Diffuse" }

在这里插入图片描述 Ramp控制颜色过渡 在这里插入图片描述 对立方体进行扩边时,轮廓线出现了断裂

断裂解决方法:在dcc软件里, 将模型连续的法线烘焙到模型的顶点色里,在进行外扩描外扩时,朝顶点色映射后的方向扩

模板缓冲实现描边

参考 Stencil Test 模板测试

基于图像处理的轮廓线渲染(屏幕后处理)

原理:该方法细分为两种边缘检测,一种是基于颜色的边缘检测,根据相邻像素颜色变化来判断,变化越大越可能是边缘点。另一种是基于法线和深度值的边缘检测,相邻位置差值越大,越可能是边缘。 优点:可以适用于任何种类的模型。 缺点:基于颜色的边缘检测会产生很多我们不希望得到的边缘线,物体的纹理、阴影等位置也被描上黑边。 基于法线和深度值的边缘检测,一些深度和法线变化很小的轮廓无法被检测出来,例如桌子上的纸张。

基于颜色的边缘检测

原理是利用一些边缘检测算子对图像进行卷积(convolution)操作,卷积操作指的就是使用一个卷积核(kernel)对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构(例如2×2、3×3的方形区域),该区域内每个方格都有一个权重值。 在这里插入图片描述 当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。卷积实际上是对卷积核覆盖的像素进行加权平均操作。 在这里插入图片描述 这是几种常见的用于边缘检测的卷积核(也被称为边缘检测算子),如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界。这种相邻像素之间的差值可以用梯度(gradient) 来表示,可以想象得到,边缘处的梯度绝对值会比较大。卷积核检测水平和竖直方向上的边缘信息,得到两个方向上的梯度值Gx和Gy,整体的梯度G = |Gx| + |Gy|,注意旧版书上的Gx和Gy写反了,这一点作者在勘误中指出。

为了实现屏幕后处理,需要把脚本绑定到相机上,获取相机看到的图像再进行处理。 首先定义屏幕后处理基类,检查一系列条件是否满足

using UnityEngine; using System.Collections; //编辑器状态下也可以执行该脚本来查看效果 [ExecuteInEditMode] [RequireComponent (typeof(Camera))] public class PostEffectsBase : MonoBehaviour { // Called when start protected void CheckResources() { bool isSupported = CheckSupport(); if (isSupported == false) NotSupported(); } // Called in CheckResources to check support on this platform protected bool CheckSupport() { if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) { Debug.LogWarning("This platform does not support image effects or render textures."); return false; } return true; } // Called when the platform doesn't support this effect protected void NotSupported() { enabled = false; } protected void Start() { CheckResources(); } // Called when need to create the material used by this effect protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) { if (shader == null) return null; if (shader.isSupported && material && material.shader == shader) return material; if (!shader.isSupported) return null; material = new Material(shader); material.hideFlags = HideFlags.DontSave; if (material) return material; return null; } }

实际的边缘检测脚本,主要是给shader传递参数的

using UnityEngine; using System.Collections; public class EdgeDetection : PostEffectsBase { //该效果需要用的Shader public Shader edgeDetectShader; private Material edgeDetectMaterial = null; public Material material { get { edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial); return edgeDetectMaterial; } } //调整边缘线强度,当edgesOnly值为0时,边缘将会叠加在原渲染图像上; //当edgesOnly值为1时,则会只显示边缘,不显示原渲染图像。 [Range(0.0f, 1.0f)] public float edgesOnly = 0.0f; public Color edgeColor = Color.black; public Color backgroundColor = Color.white; /// /// Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后, /// 再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上,OnRenderImage函数会在所有的 /// 不透明和透明的Pass执行完毕后被调用 /// void OnRenderImage (RenderTexture src, RenderTexture dest) { if (material != null) { material.SetFloat("_EdgeOnly", edgesOnly); material.SetColor("_EdgeColor", edgeColor); material.SetColor("_BackgroundColor", backgroundColor); //使用材质对src纹理进行处理,src纹理会被传递给Shader中名为_MainTex的纹理属性。 Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } }

使用Sobel算子来实现边缘检测

Shader "MyCustom/Edge Detection" { Properties { _MainTex ("[纹理] Base (RGB)", 2D) = "white" {} //当edgesOnly值为0时,边缘将会叠加在原图上;当值为1时,则会只显示边缘,不显示原渲染图像 _EdgeOnly ("[边缘线强度] Edge Only", Float) = 1.0 _EdgeColor ("[描边颜色] Edge Color", Color) = (0, 0, 0, 1) _BackgroundColor ("[背景颜色] Background Color", Color) = (1, 1, 1, 1) } SubShader { Pass { //屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,关闭深度写入, //是为了防止它“挡住”在其后面被渲染的物体。这些设置可以认为是用于屏幕后处理的标配 ZTest Always Cull Off ZWrite Off CGPROGRAM #include "UnityCG.cginc" #pragma vertex vert #pragma fragment fragSobel sampler2D _MainTex; //xxx_TexelSize是Unity为我们提供的访问xxx纹理对应的每个纹素的大小 //例如,一张512×512大小的纹理,该值大约为0.001953(即1/512) uniform half4 _MainTex_TexelSize; fixed _EdgeOnly; fixed4 _EdgeColor; fixed4 _BackgroundColor; struct v2f { float4 pos : SV_POSITION; half2 uv[9] : TEXCOORD0; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1); //当前像素左下角 o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1); o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1); o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0); o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0); o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0); o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1); o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1); o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1); return o; } //计算该像素对应的亮度 fixed Luminance(fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } //计算梯度值 half Sobel(v2f i) { const half Gx[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1}; const half Gy[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1}; half texColor; half edgeX = 0; half edgeY = 0; for (int it = 0; it


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3