【Unity3D】基于模板测试和顶点膨胀的描边方法

news2024/9/26 1:25:22

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 viewPosition = UnityObjectToViewPos(input.vertex); // 相机坐标系下的顶点坐标
                float3 viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, normal)); // 相机坐标系下的法线向量
                // 裁剪坐标系下的顶点坐标, 将顶点坐标沿着法线方向向外延伸, 延伸的部分就是描边部分
                // 乘以(-viewPosition.z)是为了抵消透视变换造成的描边宽度近大远小效果, 使得物体无论距离相机多远, 描边宽度都不发生变化
                // 除以1000是为了将描边宽度单位转换到1mm(这里的宽度是世界坐标系中的宽度, 而不是屏幕上的宽度)
                output.position = UnityViewToClipPos(viewPosition + viewNormal * _OutlineWidth * (-viewPosition.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 推荐阅读

  •  渲染管线
  • 固定管线着色器一
  • 固定管线着色器二
  • 表面着色器
  • 顶点和片段着色器
  • 选中物体描边特效
  • 水波特效
  • 半球卷屏特效
  • 卷轴特效 

 

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

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

相关文章

简单使用tomcat查看版本信息等·

Tomcat 是什么 &#xff1f;1.Apache Tomcat 是由 Apache Software Foundation&#xff08;ASF&#xff09;开发的一个开源 Java WEB 应用服务器。2.由于 Tomcat 是由 Java 语言实现的&#xff0c;因此需要运行在 Java 虚拟机上&#xff0c;所以使用前要先安装 JDK&#xff0c;…

JNPF Java3.4.5 .NET6 3.4.5 框架源码 JeeSite快速开发平台

JeeSite JeeSite 快速开发平台的主要目的是能够让初级的研发人员快速的开发出复杂的业务功能&#xff0c;中高级人员有时间做一些更有用的事情。让开发者注重专注业务&#xff0c;其余有平台来封装技术细节&#xff0c;降低技术难度&#xff0c;从而节省人力成本&#xff0c;缩…

电商控价,为什么要找控价公司

刚刚做控价的时候&#xff0c;相信我们心中都有一个疑问&#xff1a;控价嘛&#xff0c;很简单&#xff0c;把标准价格发给低价店铺&#xff0c;让他调价不就行了&#xff1f;不配合&#xff0c;我就投诉&#xff0c;要么我就断货&#xff0c;自己产品自己说了还不算吗&#xf…

《大话数据结构》读书笔记---第一章 数据结构绪论

数据结构&#xff1a;是相互之间存在一种或多种特定关系的数据元素的集合。数据结构是一门研究非数值计算的程序设计问题中的操作对象&#xff0c;以及它们之间关系和操作等相关问题的学科。程序设计数据结构算法数据&#xff1a;是描述客观事物的符号&#xff0c;是计算机中可…

C++复数类——运算符重载和类的传递

复数&#xff1a;我们把形如abi&#xff08;a,b均为实数&#xff09;的数称为复数&#xff0c;其中a称为实部&#xff0c;b称为虚部&#xff0c;i称为虚数单位。当虚部等于零时&#xff0c;这个复数可以视为实数&#xff1b;当z的虚部不等于零时&#xff0c;实部等于零时&#…

【Kotlin】类的继承 ② ( 使用 is 运算符进行类型检测 | 使用 as 运算符进行类型转换 | 智能类型转换 | Any 超类 )

文章目录一、使用 is 运算符进行类型检测二、使用 as 运算符进行类型转换 ( 智能类型转换 )三、Any 超类一、使用 is 运算符进行类型检测 在 Kotlin 中 , 如果不确定一个 实例对象的类型 , 可以 使用 is 运算符进行判定 , 使用方法 实例对象 is 判定类型上述用法可以判定 实例…

Windows11快速入门

1、如何快速上手Win11 Win11官网&#xff1a;https://www.microsoft.com/zh-cn/windows/windows-11?r1 Windows 帮助和学习&#xff1a;https://support.microsoft.com/zh-cn/windows?uizh-CN&rszh-CN&adCN) 2、Win11常用快捷键 2.1、文本编辑快捷键 功能描述快捷键…

【JavaGuide面试总结】MySQL篇·下

【JavaGuide面试总结】MySQL篇下1.介绍一下索引吧2.索引的优缺点3.讲一下索引的底层数据结构Hash 表B 树& B树1.介绍一下索引吧 索引是一种用于快速查询和检索数据的数据结构&#xff0c;其本质可以看成是一种排序好的数据结构。 索引的作用就相当于书的目录。打个比方: …

SpringBoot集成Quartz实现定时任务的动态创建、启动、暂停、恢复、删除

看了好多文章&#xff0c;都只讲了基础的demo用法&#xff0c;也就是简单的创建运行定时任务&#xff0c;对定时任务的管理却很少。 我这里从0开始搭建一个简单的demo&#xff0c;包括定时任务的各种操作&#xff0c;以及API的一些用法&#xff0c;可以实现大多场景的需求。如…

A-Star算法探索和实现(四)

本篇摘要上一篇我们针对障碍物的“死胡同”问题进行了探索&#xff0c;本篇则针对障碍物生成在网格对角顶点处是否有意义以及路径周围存在障碍物时按照对角线移动是否合理两个问题进行探讨&#xff0c;这两个问题主要在于移动规则的限定&#xff0c;主要分为以下两种情况&#…

uniapp富文本内容样式改变处理方式

前言 在日常工作中经常会遇到富文本内容渲染的需求,最常见的问题是页面样式调整好之后,对服务端返回的富文本内容进行渲染时图片显示会有问题,原因在于富文本中的图片信息没有样式处理或是添加样式与父组件中样式不一致.正常显示: 异常显示: 下面就讲一下对于富文本图片样式修…

Jenkins企业邮箱的配置和发送(win版)

在学习jenkins时&#xff0c;我把jenkins安装在自己的笔记本电脑上&#xff0c;以下的配置过程均在win10下进行。 一&#xff1a;安装邮件扩展插件 进入Dashboard——>Manage Jenkins——Manage Plugins——可选插件下选择Email Extension Plugin并“install without rest…

CUDA编程笔记(3)

文章目录前言1.CUDA程序运行时的错误检测检测运行错误的头文件检查运行时的CUDA的api函数检查运行时的CUDA的核函数CUDA-MEMCHECK工具总结前言 CUDA程序运行时的错误检测 1.CUDA程序运行时的错误检测 检测运行错误的头文件 像一些日志文件&#xff0c;一般检测错误都会编写…

浅谈C#多线程 异步编程

背景&#xff1a;线程&#xff08;英语&#xff1a;thread&#xff09;是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流&#xff0c;一个进程中可以并发多个线程&#xff0c;每条线程…

leetcode-11:1814. 统计一个数组中好对子的数目

原题为&#xff1a; 给你一个数组 nums &#xff0c;数组中只包含非负整数。定义 rev(x) 的值为将整数 x 各个数字位反转得到的结果。比方说 rev(123) 321 &#xff0c; rev(120) 21 。我们称满足下面条件的下标对 (i, j) 是 好的 &#xff1a; 0 < i < j < nums.l…

由浅入深使用validation框架进行参数校验

1 引言 平时在业务开发过程中 controller 层的参数校验有时会存在下面这样的判断 public String add(UserVO userVO) {if(userVO.getAge() null){return "年龄不能为空";}if(userVO.getAge() > 120){return "年龄不能超过120";}if(userVO.getName().is…

Mysql安全之权限用户管理参考手册

一、背景 日常Mysql维护过程中&#xff0c;基于安全要求和规定&#xff0c;需要对Mysql进行分权&#xff0c;接入金库、账户密码满足16位复杂度&#xff0c;对特定表授权&#xff0c;只读用户&#xff0c;最小化权限等处理&#xff1b;本文简要梳理下常用命令操作&#xff0c;…

vue2.x中$attrs的使用

最近笔者在做大屏项目的时候&#xff0c;由于组件数据传递&#xff0c;一层传递一层&#xff0c;使用vuex或者pinia又显得过于笨重。故而想起了那个传说中的v-bind"$attrs"以及v-on"$listeners"&#xff0c;下面就来聊下使用&#xff1a; 上图组件之间的关…

2022年度盘点|FinClip 扩展SDK推荐

2022年&#xff0c;FinClip 团队进行了24个产品迭代&#xff0c;为了丰富FinClip 的平台能力&#xff0c;除了核心SDK之外&#xff0c;我们还为开发者们提供了扩展SDK&#xff0c;扩展SDK是一个依赖核心SDK的库&#xff0c;里面提供了核心SDK中所没有的各种小程序API。 官方希…

[从零开始]用python制作识图翻译器·四

具体实现 整个工程文件已经上传到我的代码仓库。 正式开始 项目结构 数据存储 因为本系统为自用而非商用&#xff0c;并且偏向功能性&#xff0c;所以直接用一个config.json文件保存所有的用户数据&#xff0c;就不用加密了。 原型设计 如图&#xff0c;本系统一共包含两个用…