Unity 2d描边基于SpriteRender,高性能的描边解决方案

目标

以Unity默认渲染管线为例,打造不需要图片内边距,描边平滑,高性能的描边解决方案

前言

在2d游戏中经常需要给2d对象添加描边,来突出强调2d对象
当你去网上查找2d描边shader,移植到项目里面,大概率会得到这个情况
如果描边的基本原理不清楚的可以看我之前的文章 文本描边
在这里插入图片描述

①出现了边缘看起来像是被截断了,这个是因为使用在超过的范围,三角形没有覆盖到,这里没有片元着色器进行渲染,所以我们要扩展三角形的顶点,同时扩展uv
图片如果有内边距,并且描边宽度较小,可能不会出现。有时即使图片内边距足够了也会出现这个情况,这是因为生成的三角形的原因(同上),在图片的导入设置MeshType设置为FullRect可以解决,如下图,但是这会增加片元着色器的负担,会有更多的片元需要渲染,唯一的好处是可以减少顶点数据的内存。这里我们为性能考虑,使用三角形渲染,扩展多边形的顶点和uv
但是如果所以要描边的物体加内边距,会增加内存消耗
②不该有描边的区域出现了描边,
在这里插入图片描述

要解决的问题

  1. 边缘被截断了=>扩展多边形的顶点和uv
  2. 描边有锯齿感=>采样次数不足,只沿着4个或8个方向采样,在outlineWidth较大时会出现问题,增多采样次数,在实际测试中,权衡效果和性能,12次最佳
  3. 描边和图片过渡处不平滑=>在原图片边缘,边缘aphla为0-1,lerp(outlineCol,col,a),a为0,显示描边,a为1显示原来的图片
  4. 不该有描边的区域出现了描边
    ①tex2D得到的a,a>0认为是描边,图片在透明部分a不完全为0导致,提高阈值即可a>0.2
    ②当outlineWidth过大导致,uv可能会偏移到1.1,即采样uv为0.1的像素,该像素a为1导致的=>C#传入原始的uv范围,超过这个范围的不采样

最终效果演示

在这里插入图片描述

代码讲解

Shader部分

tex2D这个采样函数十分消耗性能,可以说,shader性能大部分由tex2D采样次数决定,在本shader中要想尽办法减少tex2D的采样
for会极大消耗性能,不使用for循环

half4 frag(g2f i) : SV_Target
{float4 col = tex2D(_MainTex, i.uv);col *= i.color;//乘以顶点颜色_ShowBound = float4(0, 0, 1, 1);col.a *= isInRange(i.uv);//扩展的uv不在原始uv范围,a设置为0float sum_a = 0;int iteration = 12;//当第一次采样a>0.9,说明片元为正常的像素,直接渲染,不采样邻近像素for (int ii = 0; ii < iteration; ++ii)//为了代码可读性,使用for,最终代码不使用for{if(sum_a<0.5)//如果采样结果累计>0.5,不进行采样,这样能减少tex采样{sum_a += SampleTex(i, ii, iteration);}}sum_a=step(0.5,sum_a)+sum_a;//a>0.5的部分认为1sum_a = saturate(sum_a);float4 outLineColor = float4(_OutlineColor.rgb, sum_a);//如果_OutlineWidth为0时,显示原来图片的颜色float a = step(_OutlineWidth, 0.001);//为0,描边区域;1,原始图片;0-1,图片边缘,用图片颜色和描边颜色插值过渡float4 finalCol = lerp(outLineColor, col,saturate(a+col.a));return finalCol;
}
float isInRange(float2 uv)
{float2 rs = step(_ShowBound.xy, uv) * step(uv, _ShowBound.zw);return rs.x * rs.y;
}
float SampleTex(g2f i, float ii, int sum)
{//使用预先计算好的结果,减少sincos的计算,将上下左右优先放在最前面,因为绝大部分描边由上下左右偏移得到,//可以大幅度减少在描边区域的采样次数,一旦上下左右采样得到a>threshold,就不会进行采样了const float OffsetX[12] = {1, 0, -1, 0, 0.866, 0.5, -0.5, -0.866, -0.866, -0.5, 0.5, 0.866};const float OffsetY[12] = {0, 1, 0, -1, 0.5, 0.866, 0.866, 0.5, -0.5, -0.866, -0.866, -0.5};float2 offset_uv = i.uv + float2(OffsetX[ii], OffsetY[ii]) * _MainTex_TexelSize.xy * _OutlineWidth;float sample_a=0;if(isInRange(offset_uv)>0)//如果偏移后的uv不在原始uv范围不进行采样,a为1{sample_a = tex2D(_MainTex, offset_uv).a;}float a = sample_a;a = step(0.2, a) * a;//采样结果<0.2时,不认为是描边return a;
}

C#部分

在解决上面的问题后,C#要解决最后的一个问题, 边缘看起来被截断了
如果使用的是FullRect渲染Sprite,是扩展矩形的顶点,问题会简单得多。可以在几何着色器geometry中扩展顶点和uv,但是苹果的Metal不支持几何着色器,而且FullRect渲染性能差,所以方案不行。
要扩展多边形的顶点,
首先要知道SpriteRender.sprite的vertices和uvs是只能读不可以修改的。
在网上找了一圈后,幸好unity提供了sprite.SetVertexAttribute这个扩展方法可以修改顶点
在Start时,设置原始的uv范围和描边宽度
ppu即n个像素对应1个单位长度m
扩展多边形得顶点,通过v[i-1]-v[i]和v[i+1]-v[i]得到PA和PB,(PA+PB).normalized得到PC,判断OP和PC方向夹角是否小于90,否则,PC取反,将点P沿PC方向偏移即可
在这里插入图片描述
因为使用sprite.SetVertexAttribute修改顶点,会自动计算修改后得uv,所以这里不需要修改uv了

void Start()
{// 获取SpriteRenderer组件和SpritespriteRenderer = GetComponent<SpriteRenderer>();sprite = spriteRenderer.sprite;spriteRenderer.material.SetVector("_ShowBound",bound);spriteRenderer.material.SetFloat("_OutlineWidth",outlineWidth);// 获取原始的顶点、三角形和UV数据originalVertices = sprite.vertices;ppu = 1/sprite.pixelsPerUnit;// 扩展顶点Vector2[] expandedVertices = ExpandVertices(originalVertices, outlineWidth);Vector3[] vertices = System.Array.ConvertAll(expandedVertices, v => (Vector3)v);NativeArray<Vector3> array = new NativeArray<Vector3>(vertices, Allocator.Temp);//将Vector3转换到NativeArray<Vector3>类型sprite.SetVertexAttribute(VertexAttribute.Position,array);
}
private void OnDestroy()//在销毁时还原到之前的顶点
{Vector3[] vertices = System.Array.ConvertAll(originalVertices, v => (Vector3)v);NativeArray<Vector3> array = new NativeArray<Vector3>(vertices, Allocator.Temp);sprite.SetVertexAttribute(VertexAttribute.Position,array);
}

sprite.vertices顶点不是按逆时针排列的,先得到一个按角度排列的顶点

private int CompareByAngle(Vector2 a, Vector2 b)
{float angleA = Mathf.Atan2(a.y, a.x);float angleB = Mathf.Atan2(b.y, b.x);return angleA.CompareTo(angleB);
}
Vector2[] ExpandVertices(Vector2[] vertices, float len)
{Vector2[] expandedVertices = new Vector2[vertices.Length];Vector2[] sortVertices = new Vector2[vertices.Length];for (int i = 0; i < sortVertices.Length; i++){sortVertices[i] = vertices[i];}//将顶点按逆时针排列Array.Sort(sortVertices, (a, b) => CompareByAngle(a, b));for (int i = 0; i < sortVertices.Length; i++){Vector2 vector2= sortVertices[i];int index = -1;for (int j = 0; j < vertices.Length; j++){Vector2 v= vertices[j];if (Vector2.Distance(v,vector2)<0.01f){index = j;//得到原来在vertices对应的索引break;}}Vector2 dir1 = sortVertices[(i + 1)% sortVertices.Length] - sortVertices[i];int index2 = (i - 1) % sortVertices.Length;if (index2 < 0){index2 = sortVertices.Length + index2;}Vector2 dir2 = sortVertices[index2] - sortVertices[i];dir1 = dir1.normalized;//得到P为原点的2个向量AP,BP,将其相加得到PC,结果和PO点乘,大于90度结果取反dir2 = dir2.normalized;Vector2 dir = (dir1 + dir2).normalized;int rs = Vector2.Dot(dir, vector2.normalized)>0 ? 1: -1;dir *= rs;//沿得到的dir偏移expandedVertices[index] = sortVertices[i] + dir * len * ppu;}return expandedVertices;
}

完整代码

Shader

Shader "Custom/SpriteOutline"
{Properties{[PerRendererData]_MainTex ("Sprite Texture", 2D) = "white" {}_OutlineWidth ("Outline Width", Range(0,30)) = 5_OutlineColor ("Outline Color", Color) = (1,1,1,1)_ShowBound("Show Bound" ,Vector)=(0,0,1,1)}SubShader{Tags{"Queue"="Transparent" "IgnoreProjector"="true" "RenderType"="Transparent"}Cull OffLighting OffZWrite OffBlend SrcAlpha OneMinusSrcAlphaPass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float4 color : COLOR;float4 uv : TEXCOORD0;float4 uv2 : TEXCOORD1;float4 tangent : TANGENT;};struct g2f{float2 uv : TEXCOORD0;half4 color : COLOR;float4 vertex : SV_POSITION;float2 lightingUV:TEXCOORD1;float2 uv2 : TEXCOORD2;float4 tangent : TANGENT;};sampler2D _MainTex;float4 _MainTex_ST;float4 _MainTex_TexelSize;float _OutlineWidth;float4 _OutlineColor;float4 _ShowBound;g2f vert(appdata v){g2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.color = v.color;o.tangent = v.tangent;o.uv2=v.uv2;o.lightingUV = half2(ComputeScreenPos(o.vertex / o.vertex.w).xy);return o;}float isInRange(float2 uv){float2 rs = step(_ShowBound.xy, uv) * step(uv, _ShowBound.zw);return rs.x * rs.y;}float SampleTex(g2f i, float ii){const float OffsetX[12] = {1, 0, -1, 0, 0.866, 0.5, -0.5, -0.866, -0.866, -0.5, 0.5, 0.866};const float OffsetY[12] = {0, 1, 0, -1, 0.5, 0.866, 0.866, 0.5, -0.5, -0.866, -0.866, -0.5};float2 offset_uv = i.uv + float2(OffsetX[ii], OffsetY[ii]) * _MainTex_TexelSize.xy * _OutlineWidth;float sample_a=0;if(isInRange(offset_uv)>0){sample_a = tex2D(_MainTex, offset_uv).a;}float a = sample_a;a = step(0.2, a) * a;return a;}half4 frag(g2f i) : SV_Target{float4 col = tex2D(_MainTex, i.uv);col *= i.color;//_ShowBound = float4(0, 0, 1, 1);_ShowBound = i.tangent;_OutlineWidth=i.uv2.x;col.a *= isInRange(i.uv);float sum_a = 0;float threshold=0.5;if(col.a<threshold){sum_a += SampleTex(i, 0);if (sum_a < threshold){sum_a += SampleTex(i, 1);if (sum_a < threshold){sum_a += SampleTex(i, 2);if (sum_a < threshold){sum_a += SampleTex(i, 3);if (sum_a < threshold){sum_a += SampleTex(i, 4);if (sum_a < threshold){sum_a += SampleTex(i, 5);if (sum_a < threshold){sum_a += SampleTex(i, 6);if (sum_a < threshold){sum_a += SampleTex(i, 7);if (sum_a < threshold){sum_a += SampleTex(i, 8);if (sum_a < threshold){sum_a += SampleTex(i, 9);if (sum_a < threshold){sum_a += SampleTex(i, 10);if (sum_a < threshold){sum_a += SampleTex(i, 11);}}}}}}}}}}}}sum_a=step(threshold,sum_a)+sum_a;sum_a = saturate(sum_a);float4 outLineColor = float4(_OutlineColor.rgb, sum_a);float a = step(_OutlineWidth, 0.001);float4 finalCol = lerp(outLineColor, col,saturate(a+col.a));return finalCol;}ENDCG}}Fallback "Sprites/Default"
}

C#

public class SpriteOutline : MonoBehaviour
{private SpriteRenderer spriteRenderer;private Sprite sprite;private Vector2[] originalVertices;public float outlineWidth = 0f;private float ppu;void Start(){// 获取SpriteRenderer组件和SpritespriteRenderer = GetComponent<SpriteRenderer>();sprite = spriteRenderer.sprite;Vector4 bound = new Vector4();Vector2[] uvs= sprite.uv;bound =new Vector4(1, 1, 0, 0);for (int i = 0; i < uvs.Length; i++){var uv = uvs[i];bound.x = Mathf.Min(bound.x, uv.x);bound.y = Mathf.Min(bound.y, uv.y);bound.z = Mathf.Max(bound.z, uv.x);bound.w = Mathf.Max(bound.w, uv.y);}//spriteRenderer.material.SetVector("_ShowBound",bound);//spriteRenderer.material.SetFloat("_OutlineWidth",outlineWidth);// 获取原始的顶点、三角形和UV数据originalVertices = sprite.vertices;ppu = 1/sprite.pixelsPerUnit;// 扩展顶点Vector2[] expandedVertices = ExpandVertices(originalVertices, outlineWidth);Vector3[] vertices = System.Array.ConvertAll(expandedVertices, v => (Vector3)v);NativeArray<Vector3> array = new NativeArray<Vector3>(vertices, Allocator.Temp);sprite.SetVertexAttribute(VertexAttribute.Position,array);Vector2[] uv2Vector4s=new Vector2[vertices.Length];for (int i = 0; i < uv2Vector4s.Length; i++){uv2Vector4s[i] =new Vector2(outlineWidth, 0);}NativeArray<Vector2> uv2s_array = new NativeArray<Vector2>(uv2Vector4s, Allocator.Temp);sprite.SetVertexAttribute(VertexAttribute.TexCoord1,uv2s_array);Vector4[] tangents=new Vector4[vertices.Length];for (int i = 0; i < tangents.Length; i++){tangents[i] = bound;}NativeArray<Vector4> tangent_array = new NativeArray<Vector4>(tangents, Allocator.Temp);sprite.SetVertexAttribute(VertexAttribute.Tangent,tangent_array);}private void OnDestroy(){Vector3[] vertices = System.Array.ConvertAll(originalVertices, v => (Vector3)v);NativeArray<Vector3> array = new NativeArray<Vector3>(vertices, Allocator.Temp);sprite.SetVertexAttribute(VertexAttribute.Position,array);}private int CompareByAngle(Vector2 a, Vector2 b){float angleA = Mathf.Atan2(a.y, a.x);float angleB = Mathf.Atan2(b.y, b.x);return angleA.CompareTo(angleB);}Vector2[] ExpandVertices(Vector2[] vertices, float len){Vector2[] expandedVertices = new Vector2[vertices.Length];Vector2[] sortVertices = new Vector2[vertices.Length];for (int i = 0; i < sortVertices.Length; i++){sortVertices[i] = vertices[i];}Array.Sort(sortVertices, (a, b) => CompareByAngle(a, b));for (int i = 0; i < sortVertices.Length; i++){Vector2 vector2= sortVertices[i];int index = -1;for (int j = 0; j < vertices.Length; j++){Vector2 v= vertices[j];if (Vector2.Distance(v,vector2)<0.01f){index = j;break;}}Vector2 dir1 = sortVertices[(i + 1)% sortVertices.Length] - sortVertices[i];int index2 = (i - 1) % sortVertices.Length;if (index2 < 0){index2 = sortVertices.Length + index2;}Vector2 dir2 = sortVertices[index2] - sortVertices[i];dir1 = dir1.normalized;dir2 = dir2.normalized;Vector2 dir = (dir1 + dir2).normalized;int rs = Vector2.Dot(dir, vector2.normalized)>0 ? 1: -1;dir *= rs;expandedVertices[index] = sortVertices[i] + dir * len * ppu;}return expandedVertices;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/66605.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【利用 Unity + Mirror 网络框架、Node.js 后端和 MySQL 数据库】

要实现一个简单的1v1战斗小游戏&#xff0c;利用 Unity Mirror 网络框架、Node.js 后端和 MySQL 数据库&#xff0c;我们可以将其分为几个主要部分&#xff1a;客户端&#xff08;Unity&#xff09;、服务器&#xff08;Node.js&#xff09;和数据库&#xff08;MySQL&#xf…

Inception模型详解及代码分析

模型背景 Inception系列模型由Google团队提出,旨在解决CNN分类模型面临的两大挑战: 如何在增加网络深度的同时提升分类性能 如何在保证分类准确率的同时降低计算和内存开销 Inception V1通过引入 并行卷积结构 和 1x1卷积 ,巧妙地解决了这两个问题,在保证模型质量的前提下…

【算法】算法大纲

这篇文章介绍计算机算法的各个思维模式。 包括 计数原理、数组、树型结构、链表递归栈、查找排序、管窥算法、图论、贪心法和动态规划、以及概率论:概率分治和机器学习。没有办法逐个说明,算法本身错综复杂,不同的算法对应着不同的实用场景,也需要根据具体情况设计与调整。…

spring mvc源码学习笔记之九

在前面的文章中&#xff0c;我们简单讲了可以用 WebApplicationInitializer 接口去替换 web.xml。 本文对这一块再做个详细讲解。 在 WebApplicationInitializer 这个接口的 javadoc 中有提到可以用继承 AbstractAnnotationConfigDispatcherServletInitializer 的方式替换实现 …

【HTML+CSS+JS+VUE】web前端教程-2-HTML5介绍和基础骨架

HTML5介绍 HTML5是用来描述网页的一种语言,被称为超文本标记语言用HTML5编写的文件,后缀以.html结尾HTML是一种标记语言标记语言是一套标记标签标签是由尖括号包围的关键字,例如:标签有两种表现形式: 双标签,例如:<html></html> 单标签,例如:<img>HTML…

单例模式-如何保证全局唯一性?

以下是几种实现单例模式并保证全局唯一性的方法&#xff1a; 1. 饿汉式单例模式 class Singleton { private:// 私有构造函数&#xff0c;防止外部创建对象Singleton() {}// 静态成员变量&#xff0c;存储单例对象static Singleton instance; public:// 公有静态成员函数&…

Oracle OCP考试常见问题之线上考试流程

首先要注意的是&#xff1a;虽然Oracle官方在国际上取消了获得OCP认证需要培训记录的要求&#xff0c;但在中国区&#xff0c;考生仍然需要参加Oracle的官方或者其合作伙伴组织的培训&#xff0c;并且由Oracle授权培训中心向Oracle提交学员培训记录。考生只有在完成培训并通过考…

基于海思soc的智能产品开发(camera sensor的两种接口)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 对于嵌入式开发设备来说&#xff0c;除了图像显示&#xff0c;图像输入也是很重要的一部分。说到图像输入&#xff0c;就不得不提到camera。目前ca…

Go语言之十条命令(The Ten Commands of Go Language)

Go语言之十条命令 Go语言简介 Go语言&#xff08;又称Golang&#xff09;‌是由Google开发的一种开源编程语言&#xff0c;首次公开发布于2009年。Go语言旨在提供简洁、高效、可靠的软件开发解决方案&#xff0c;特别强调并发编程和系统编程‌。 Go语言的基本特征 ‌静态强类…

Redis 笔记(二)-Redis 安装及测试

一、什么是 Redis 中文网站 Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务&#xff0c;是一个开源的使用 ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value&#xff0c;并提供多种语言的 API。 Redis 开源&#xff0c;遵循 BSD 基…

在 PhpStorm 中配置命令行直接运行 PHP 的步骤

在 PhpStorm 中配置命令行直接运行 PHP 的步骤如下&#xff1a; ### 1. 安装 PHP 并配置环境变量 确保你已经在系统上安装了 PHP&#xff0c;并且将 PHP 的可执行文件路径添加到系统的环境变量中。这样你可以在命令行中直接使用 php 命令。 ### 2. 配置 PhpStorm 的 PHP 解释…

H2数据库在单元测试中的应用

H2数据库特征 用比较简洁的话来介绍h2数据库&#xff0c;就是一款轻量级的内存数据库&#xff0c;支持标准的SQL语法和JDBC API&#xff0c;工业领域中&#xff0c;一般会使用h2来进行单元测试。 这里贴一下h2数据库的主要特征 Very fast database engineOpen sourceWritten…

数据库中锁与ETL的故障排除和性能优化

锁的类型 共享锁&#xff08;Shared Lock&#xff0c;S锁&#xff09;&#xff1a;又称读锁&#xff0c;允许事务对数据进行读取操作&#xff0c;多个事务可同时获取同一资源的共享锁&#xff0c;不会互相阻塞&#xff0c;用于并发读操作。排他锁&#xff08;Exclusive Lock&a…

【设计模式】装饰器与代理模式的对比

文章目录 装饰器模式&#xff08;Decorator Pattern&#xff09;代理模式&#xff08;Proxy Pattern&#xff09;两者之间的区别 装饰器模式&#xff08;Decorator Pattern&#xff09; 装饰器模式是一种结构型设计模式&#xff0c;它允许你动态地将责任附加到对象上&#xff…

通俗易懂之线性回归时序预测PyTorch实践

线性回归&#xff08;Linear Regression&#xff09;是机器学习中最基本且广泛应用的算法之一。它不仅作为入门学习的经典案例&#xff0c;也是许多复杂模型的基础。本文将全面介绍线性回归的原理、应用&#xff0c;并通过一段PyTorch代码进行实践演示&#xff0c;帮助读者深入…

安全基础-互联网技术基础

互联网技术基础 概述&#xff1a;计算机网络、网络协议、HTTP协议、前端与后端技术、Web服务器、数据库以及浏览器等 目录 互联网技术基础前言一、计算机网络定义二、网络协议和协议分层1.OSI七层模型2.TCP/IP四层模型 三、HTTP协议1、HTTP协议的特点2、HTTP请求3、HTTP响应4、…

MATLAB深度学习实战文字识别

文章目录 前言视频演示效果1.DB文字定位环境配置安装教程与资源说明1.1 DB概述1.2 DB算法原理1.2.1 整体框架1.2.2 特征提取网络Resnet1.2.3 自适应阈值1.2.4 文字区域标注生成1.2.5 DB文字定位模型训练 2.CRNN文字识别2.1 CRNN概述2.2 CRNN原理2.2.1 CRNN网络架构实现2.2.2 CN…

和为0的四元组-蛮力枚举(C语言实现)

目录 一、问题描述 二、蛮力枚举思路 1.初始化&#xff1a; 2.遍历所有可能的四元组&#xff1a; 3.检查和&#xff1a; 4.避免重复&#xff1a; 5.更新计数器&#xff1a; 三、代码实现 四、运行结果 五、 算法复杂度分析 一、问题描述 给定一个整数数组 nums&…

SpringBoot日常:集成Kafka

文章目录 1、pom.xml文件2、application.yml3、生产者配置类4、消费者配置类5、消息订阅6、生产者发送消息7、测试发送消息 本章内容主要介绍如何在springboot项目对kafka进行整合&#xff0c;最终能达到的效果就是能够在项目中通过配置相关的kafka配置&#xff0c;就能进行消息…

【实用技能】如何使用 .NET C# 中的 Azure Key Vault 中的 PFX 证书对 PDF 文档进行签名

TX Text Control 是一款功能类似于 MS Word 的文字处理控件&#xff0c;包括文档创建、编辑、打印、邮件合并、格式转换、拆分合并、导入导出、批量生成等功能。广泛应用于企业文档管理&#xff0c;网站内容发布&#xff0c;电子病历中病案模板创建、病历书写、修改历史、连续打…