1 前言

选中物体描边特效 中介绍了基于模板纹理模糊膨胀的描边方法,该方法实现了软描边,效果较好,但是为了得到模糊纹理,对屏幕像素进行了多次渲染,效率欠佳。本文将介绍另一种描边方法:基于模板测试和顶点膨胀的描边方法,该方法绘制的是硬描边,但效率较高。

​ 基于顶点膨胀的描边方法都会遇到以下问题:

  • 法线突变处(如:立方体的两面交界处),描边断裂
  • 描边宽度受透视影响,远处描边较窄,近处描边较宽

​ 本文通过平滑法线解决描边断裂物体,通过深度信息抵消透视对描边宽度的影响。

​ 本文代码见→基于模板测试和顶点膨胀的描边方法

2 原理

1)概述

​ 在 SubShader 中开 2 个 Pass 渲染通道,第一个 Pass 通道将待描边物体的屏幕区域像素对应的模板值标记为 1,第二个 Pass 通道将待描边物体的顶点向外膨胀,绘制模板值为非 1 的膨胀区域,即外环区域。

2)原图

3)模板

​ 说明:由于第一个 Pass 通道只需要标记模板值,不需要渲染颜色,因此可以通过 "ColorMask 0" 过滤掉颜色。

4)膨胀外环

5)合成纹理

3 代码实现

​ SelectController.cs

using System.Collections.Generic;
using UnityEngine; public class SelectController : MonoBehaviour { // 单击选中控制
private List<GameObject> targets; // 选中的游戏对象
private List<GameObject> loseFocus; // 失焦的游戏对象
private RaycastHit hit; // 碰撞信息 private void Awake() {
targets = new List<GameObject>();
loseFocus = new List<GameObject>();
} private void Update() {
if (Input.GetMouseButtonUp(0)) {
GameObject hitObj = GetHitObj();
if (hitObj == null) { // 未选中任何物体, 已描边的全部取消描边
targets.ForEach(obj => loseFocus.Add(obj));
targets.Clear();
}
else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) {
if (targets.Contains(hitObj)) { // Ctrl重复选中, 取消描边
loseFocus.Add(hitObj);
targets.Remove(hitObj);
} else { // Ctrl追加描边
targets.Add(hitObj);
}
} else { // 单选描边
targets.ForEach(obj => loseFocus.Add(obj));
targets.Clear();
targets.Add(hitObj);
loseFocus.Remove(hitObj);
}
DrawOutline();
}
} private void DrawOutline() { // 绘制描边
targets.ForEach(obj => {
if (obj.GetComponent<OutlineEffect>() == null) {
obj.AddComponent<OutlineEffect>();
} else {
obj.GetComponent<OutlineEffect>().enabled = true;
}
});
loseFocus.ForEach(obj => {
if (obj.GetComponent<OutlineEffect>() != null) {
obj.GetComponent<OutlineEffect>().enabled = false;
}
});
loseFocus.Clear();
} private GameObject GetHitObj() { // 获取屏幕射线碰撞的物体
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit)) {
return hit.transform.gameObject;
}
return null;
}
}

​ OutlineEffect.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine; [DisallowMultipleComponent]
public class OutlineEffect : MonoBehaviour { // 描边特效
private Renderer[] renderers; // 当前对象及其子对象的渲染器
private Material outlineMaterial; // 描边材质 private void Awake() {
renderers = GetComponentsInChildren<Renderer>();
outlineMaterial = new Material(Shader.Find("MyShader/OutlineEffect"));
LoadSmoothNormals();
} private void OnEnable() {
outlineMaterial.SetFloat("_StartTime", Time.timeSinceLevelLoad * 2);
foreach (var renderer in renderers) {
List<Material> materials = renderer.sharedMaterials.ToList();
materials.Add(outlineMaterial);
renderer.materials = materials.ToArray();
}
} private void OnDisable() {
foreach (var renderer in renderers) {
// 这里只能用sharedMaterials, 使用materials会进行深拷贝, 使得删除材质会失败
List<Material> materials = renderer.sharedMaterials.ToList();
materials.Remove(outlineMaterial);
renderer.materials = materials.ToArray();
}
} private void LoadSmoothNormals() { // 加载平滑的法线(对相同顶点的所有法线取平均值)
foreach (var meshFilter in GetComponentsInChildren<MeshFilter>()) {
List<Vector3> smoothNormals = SmoothNormals(meshFilter.sharedMesh);
meshFilter.sharedMesh.SetUVs(3, smoothNormals); // 将平滑法线存储到UV3中
var renderer = meshFilter.GetComponent<Renderer>();
if (renderer != null) {
CombineSubmeshes(meshFilter.sharedMesh, renderer.sharedMaterials.Length);
}
}
foreach (var skinnedMeshRenderer in GetComponentsInChildren<SkinnedMeshRenderer>()) {
// 清除SkinnedMeshRenderer的UV3
skinnedMeshRenderer.sharedMesh.uv4 = new Vector2[skinnedMeshRenderer.sharedMesh.vertexCount];
CombineSubmeshes(skinnedMeshRenderer.sharedMesh, skinnedMeshRenderer.sharedMaterials.Length);
}
} private List<Vector3> SmoothNormals(Mesh mesh) { // 计算平滑法线, 对相同顶点的所有法线取平均值
// 按照顶点进行分组(如: 立方体有8个顶点, 但网格实际存储的是24个顶点, 因为相交的3个面的法线不同, 所以一个顶点存储了3次)
var groups = mesh.vertices.Select((vertex, index) => new KeyValuePair<Vector3, int>(vertex, index)).GroupBy(pair => pair.Key);
List<Vector3> smoothNormals = new List<Vector3>(mesh.normals);
foreach (var group in groups) {
if (group.Count() == 1) {
continue;
}
Vector3 smoothNormal = Vector3.zero;
foreach (var pair in group) { // 计算法线均值(如: 对立方体同一顶点的3个面的法线取平均值, 平滑法线沿对角线向外)
smoothNormal += smoothNormals[pair.Value];
}
smoothNormal.Normalize();
foreach (var pair in group) { // 平滑法线赋值(如: 立方体的同一顶点的3个面的平滑法线都是沿着对角线向外)
smoothNormals[pair.Value] = smoothNormal;
}
}
return smoothNormals;
} private void CombineSubmeshes(Mesh mesh, int materialsLength) { // 绑定子网格
if (mesh.subMeshCount == 1) {
return;
}
if (mesh.subMeshCount > materialsLength) {
return;
}
mesh.subMeshCount++;
mesh.SetTriangles(mesh.triangles, mesh.subMeshCount - 1);
}
}

​ OutlineEffect.shader

Shader "MyShader/OutlineEffect" {
Properties {
_OutlineWidth("Outline Width", Range(0, 10)) = 8
_StartTime ("startTime", Float) = 0 // _StartTime用于控制每个选中的对象颜色渐变不同步
} SubShader {
Tags {
// 渲染队列: Background(1000, 后台)、Geometry(2000, 几何体, 默认)、Transparent(3000, 透明)、Overlay(4000, 覆盖)
"Queue" = "Transparent+110"
"RenderType" = "Transparent"
"DisableBatching" = "True"
} // 将待描边物体的屏幕区域像素对应的模板值标记为1
Pass {
Cull Off // 关闭剔除渲染, 取值有: Off、Front、Back, Off表示正面和背面都渲染
ZTest Always // 总是通过深度测试, 使得物体即使被遮挡时, 也能生成模板
ZWrite Off // 关闭深度缓存, 避免该物体遮挡前面的物体
ColorMask 0 // 允许通过的颜色通道, 取值有: 0、R、G、B、A、RGBA的组合(RG、RGB等), 0表示不渲染颜色 Stencil { // 模板测试, 只有通过模板测试的像素才会渲染
Ref 1 // 设定参考值为1
Pass Replace // 如果通过模板测试, 将像素的模板值设置为参考值(1), 模板值的初值为0, 没有Comp表示总是通过模板测试
}
} // 绘制模板标记外的物体像素, 即膨胀的外环上的像素
Pass {
Cull Off // 关闭剔除渲染, 取值有: Off、Front、Back, Off表示正面和背面都渲染
ZTest Always // 总是通过深度测试, 使得物体即使被遮挡时, 也能生成描边
ZWrite Off // 关闭深度缓存, 避免该物体遮挡前面的物体
Blend SrcAlpha OneMinusSrcAlpha // 混合测试, 与背后的物体颜色混合
ColorMask RGB // 允许通过的颜色通道, 取值有: 0、R、G、B、A、RGBA的组合(RG、RGB等), 0表示不渲染颜色 Stencil { // 模板测试, 只有通过模板测试的像素才会渲染
Ref 1 // 设定参考值为1
Comp NotEqual // 这里只有模板值为0的像素才会通过测试, 即只有膨胀的外环上的像素能通过模板测试
} CGPROGRAM
#include "UnityCG.cginc" #pragma vertex vert
#pragma fragment frag uniform float _OutlineWidth;
uniform float _StartTime; struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
float3 smoothNormal : TEXCOORD3; // 平滑的法线, 对相同顶点的所有法线取平均值
}; struct v2f {
float4 position : SV_POSITION;
}; v2f vert(appdata input) {
v2f output;
float3 normal = any(input.smoothNormal) ? input.smoothNormal : input.normal; // 光滑的法线
float3 viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, normal)); // 观察坐标系下的法线向量
float3 viewPos = UnityObjectToViewPos(input.vertex); // 观察坐标系下的顶点坐标
// 裁剪坐标系下的顶点坐标, 将顶点坐标沿着法线方向向外延伸, 延伸的部分就是描边部分
// 乘以(-viewPos.z)是为了抵消透视变换造成的描边宽度近大远小效果, 使得物体无论距离相机多远, 描边宽度都不发生变化
// 除以1000是为了将描边宽度单位转换到1mm(这里的宽度是世界坐标系中的宽度, 而不是屏幕上的宽度)
output.position = UnityViewToClipPos(viewPos + viewNormal * _OutlineWidth * (-viewPos.z) / 1000);
return output;
} fixed4 frag(v2f input) : SV_Target {
float t1 = sin(_Time.z - _StartTime); // _Time = float4(t/20, t, t*2, t*3)
float t2 = cos(_Time.z - _StartTime);
// 描边颜色随时间变化, 描边透明度随时间变化, 视觉上感觉描边在膨胀和收缩
return float4(t1 + 1, t2 + 1, 1 - t1, 1 - t2);
} ENDCG
}
}
}

4 运行效果

5 推荐阅读

​ 声明:本文转自【Unity3D】基于模板测试和顶点膨胀的描边方法

【Unity3D】基于模板测试和顶点膨胀的描边方法的更多相关文章

  1. 阶段5 3.微服务项目【学成在线】_day04 页面静态化_10-freemarker静态化测试-基于模板文件静态化

    把resource拷贝到test目录下 只保留文件夹结构和test1.ftl这个模板文件就可以了. 新建一个包 编写测试类 使用freemaker提供的方法生成静态文件 Configuration是i ...

  2. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十一章:模板测试 代码工程地址: https://github.co ...

  3. OpenGL-----深度测试,剪裁测试、Alpha测试和模板测试

    片断测试其实就是测试每一个像素,只有通过测试的像素才会被绘制,没有通过测试的像素则不进行绘制.OpenGL提供了多种测试操作,利用这些操作可以实现一些特殊的效果.我们在前面的课程中,曾经提到了“深度测 ...

  4. Shader 模板缓冲和模板测试

    http://blog.sina.com.cn/s/blog_6e159df70102xa67.html 模板缓冲的概念 Unity官方的Shader文档根本没有提到这个玩意,这个概念也是看到了UGU ...

  5. 时序分析:DTW算法(基于模板)

    对时序对象进行分析,使用KMP算法可以分析速率不变的模式,参考时序分析:欧式空间轨迹模式识别.使用基于模板匹配的方法,对于速率发生变化的模式,需要用新的对速率要求松散的方法,DTW方法为一种广泛使用的 ...

  6. OpenGL利用模板测试实现不规则裁剪

    本文是原创文章,如需转载,请注明文章出处 在游戏开发中,经常会有这样的需求:给定一张64x64的卡牌素材,要求只显示以图片中心为圆点.直径为64的圆形区域,这就要用到模板测试来进行不规则裁剪. 实现不 ...

  7. OpenGL ES 中的模板测试

    模板测试的主要功能是丢弃一部分片元,相对于深度检测来说,模板测试提出的片元数量相对较少.模板测试发生在剪裁测试之后,深度测试之前. 使用模板测试时很重要的代码提示: 1.glClear( GL_STE ...

  8. Unity3d 基于物理渲染Physically-Based Rendering之最终篇

    前情提要: 讲求基本算法 Unity3d 基于物理渲染Physically-Based Rendering之specular BRDF plus篇 Unity3d 基于物理渲染Physically-B ...

  9. PGM:基于模板的表示

    http://blog.csdn.net/pipisorry/article/details/52537660 引言 概率图模型(无论贝叶斯网或马尔可夫网)在一个固定的随机变量集X上具体指定了一个联合 ...

  10. [UnityShader基础]05.模板测试

    参考链接: https://blog.csdn.net/u011047171/article/details/46928463 https://blog.csdn.net/JohnBlu/articl ...

随机推荐

  1. 05-Shell索引数组变量

    1.介绍 Shell 支持数组(Array),数组是若干数据的集合,其中的每一份数据都称为数组的元素. 注意Bash Shell 只支持一维数组,不支持多维数组. 2.数组的定义 2.1 语法 在 S ...

  2. SpringMVC02——第一个MVC程序-注解版(high版!!!!)

    注解版 新建一个子项目,添加Web支持 在pom.xml文件中引入相关的依赖:主要引入Spring框架核心库.SpringMVC.servlet,JSTL等. 创建实体类Fruit package c ...

  3. [转帖]jmeter正则表达式提取器获取数组数据-02篇

    接上篇,当我们正则表达式匹配到多个值以后,入下图所示,匹配到21个结果,如果我们想一次拿到这一组数据怎么办呢 打开正则表达式提取器页面,匹配数字填入-1即可 通过调试取样器就可以看到匹配到已经匹配到多 ...

  4. [转帖]什么是拒绝服务(DoS)攻击?

    https://www.cloudflare.com/zh-cn/learning/ddos/glossary/denial-of-service/ 什么是拒绝服务攻击? 拒绝服务(DoS)攻击是一种 ...

  5. Ubuntu2204设置固定IP地址

    前言 Ubuntu每次升级都会修改一部分组件. 从1804开始Ubuntu开始使用netplan的方式进行网络设置. 但是不同版本的配置一直在升级与变化. 今天掉进坑里折腾了好久. 所以这边总结一下, ...

  6. 【JS 逆向百例】房天下登录接口参数逆向

    声明 本文章中所有内容仅供学习交流,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 逆向目标 目标:房天下账号密码登录 主页:https://passpo ...

  7. linux如何配置ssh密钥登录

    为什么要用ssh密钥登录 购买的服务器设置密码很容易被暴力破解,用密钥登录安全很多.root用户新建的用户也要用密钥登录更安全,如果一直su - 用户名登录 不方便 用xftp等服务上传文件到用户使用 ...

  8. CCFLOW源码解读系列01-----发起流程

    1.发起流程 发起流程时主要做了两件事:一是写入业务数据表,二是新建一条审批流程记录. 发起流程的方法 public static Int64 Node_CreateStartNodeWork(str ...

  9. windwos10任务栏居中

    如下操作 新建一个文件夹如图 然后出现这个重右往左一直拖然后拉出来就行了如图 拖不动或者没有的把这个关了-锁定任务栏 文字如何隐藏? 在这个文字旁边右击关闭标题即可 然后锁定任务栏就OK了

  10. # 重要-即时通讯IM开源项目OpenIM关于版本管理及v2.3.0发布计划

    越来越多的客户把OpenIM用到了生产环境,由于新特性持续迭代和bug修复,会涉及到后续的升级方案,为了让大家后续从容应对,本文重点总结OpenIM对未来版本管理的思路和方案.同时,官网对于文档进行了 ...