【Unity3D】基于模板测试和顶点膨胀的描边方法
创始人
2024-05-13 19:15:40
0

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 targets; // 选中的游戏对象private List loseFocus; // 失焦的游戏对象private RaycastHit hit; // 碰撞信息private void Awake() {targets = new List();loseFocus = new List();}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() == null) {obj.AddComponent();} else {obj.GetComponent().enabled = true;}});loseFocus.ForEach(obj => {if (obj.GetComponent() != null) {obj.GetComponent().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();outlineMaterial = new Material(Shader.Find("MyShader/OutlineEffect"));LoadSmoothNormals();}private void OnEnable() {outlineMaterial.SetFloat("_StartTime", Time.timeSinceLevelLoad * 2);foreach (var renderer in renderers) {List materials = renderer.sharedMaterials.ToList();materials.Add(outlineMaterial);renderer.materials = materials.ToArray();}}private void OnDisable() {foreach (var renderer in renderers) {// 这里只能用sharedMaterials, 使用materials会进行深拷贝, 使得删除材质会失败List materials = renderer.sharedMaterials.ToList();materials.Remove(outlineMaterial);renderer.materials = materials.ToArray();}}private void LoadSmoothNormals() { // 加载平滑的法线(对相同顶点的所有法线取平均值)foreach (var meshFilter in GetComponentsInChildren()) {List smoothNormals = SmoothNormals(meshFilter.sharedMesh);meshFilter.sharedMesh.SetUVs(3, smoothNormals); // 将平滑法线存储到UV3中var renderer = meshFilter.GetComponent();if (renderer != null) {CombineSubmeshes(meshFilter.sharedMesh, renderer.sharedMaterials.Length);}}foreach (var skinnedMeshRenderer in GetComponentsInChildren()) {// 清除SkinnedMeshRenderer的UV3skinnedMeshRenderer.sharedMesh.uv4 = new Vector2[skinnedMeshRenderer.sharedMesh.vertexCount];CombineSubmeshes(skinnedMeshRenderer.sharedMesh, skinnedMeshRenderer.sharedMaterials.Length);}}private List SmoothNormals(Mesh mesh) { // 计算平滑法线, 对相同顶点的所有法线取平均值// 按照顶点进行分组(如: 立方体有8个顶点, 但网格实际存储的是24个顶点, 因为相较的3个面的法线不同, 所以一个顶点存储了3次)var groups = mesh.vertices.Select((vertex, index) => new KeyValuePair(vertex, index)).GroupBy(pair => pair.Key);List smoothNormals = new List(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"}// 将待描边物体的屏幕区域像素对应的模板值标记为1Pass {Cull Off // 关闭剔除渲染, 取值有: Off、Front、Back, Off表示正面和背面都渲染ZTest Always // 总是通过深度测试, 使得物体即使被遮挡时, 也能生成模板ZWrite Off // 关闭深度缓存, 避免该物体遮挡前面的物体ColorMask 0 // 允许通过的颜色通道, 取值有: 0、R、G、B、A、RGBA的组合(RG、RGB等), 0表示不渲染颜色Stencil { // 模板测试, 只有通过模板测试的像素才会渲染Ref 1 // 设定参考值为1Pass 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 // 设定参考值为1Comp NotEqual // 这里只有模板值为0的像素才会通过测试, 即只有膨胀的外环上的像素能通过模板测试}CGPROGRAM#include "UnityCG.cginc"#pragma vertex vert#pragma fragment fraguniform 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 推荐阅读

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

相关内容

热门资讯

我的家乡初三的作文【通用6篇... 我的家乡初三的作文 篇一:我眼中的家乡家乡是一个让我无比怀念的地方。它位于一个山水相依的地方,风景如...
初中初三作文800字:得失寸... 初中初三作文800字:得失寸心知 篇一得失寸心知在我们的生活中,我们经常会遇到得失的情况。有些人总是...
找回自我作文500字初三(推... 找回自我作文500字初三 篇一随着社交媒体的普及和学业的压力增加,初三学生们往往会迷失自我。他们可能...
游岳阳楼作文(经典3篇) 游岳阳楼作文 篇一游岳阳楼作文岳阳楼,位于湖南省岳阳市岳阳楼区洞庭湖畔,是中国古代建筑的瑰宝之一。游...
初三作文真情永驻(经典3篇) 初三作文真情永驻 篇一初三作文真情永驻初三,是我们学生生涯中最为重要的一年。这一年,我们要面对升学压...
初三朋友作文600字【实用6... 初三朋友作文600字 篇一:我与朋友的经历初三,是我们人生中重要的一年。作为初三学生,我们面临着巨大...
馄饨的味道初三作文【经典3篇... 馄饨的味道初三作文 篇一:回味无穷的馄饨之旅作为中国传统的美食之一,馄饨以其独特的味道和制作工艺赢得...
初三新学期的打算作文600字... 初三新学期的打算作文600字 篇一新学期开始了,我心中充满了期待和憧憬。初三,是我人生中的重要一年,...
回家的旅途初三作文(通用3篇... 回家的旅途初三作文 篇一回家的旅途初三作文初三的寒假,对于我来说意味着一个特殊的时刻——回家。这是我...
含笑奔跑的少年初三优秀作文【... 含笑奔跑的少年初三优秀作文 篇一初三是每个中学生的重要时刻,是他们人生中的一个转折点。而在这个转折点...
幻想与现实初三作文【优选3篇... 幻想与现实初三作文 篇一标题:幻想与现实幻想与现实是两个截然不同的概念,它们在我们的生活中起着不同的...
我的初三生活作文【精简6篇】 我的初三生活作文 篇一初三生活对于每个初中学生来说都是一个重要的时期。在这一年里,我经历了许多新的挑...
我多想轻轻地抱着你初三抒情作... 我多想轻轻地抱着你初三抒情作文 篇一初三这一年,是我们成长的最后一年,也是我们最宝贵的时光。每一天,...
春天初三作文(最新6篇) 春天初三作文 篇一:春天的美丽春天是一年四季中最美丽的季节,它带来了温暖的阳光、绿意盎然的景色和婉约...
初三学生作文(最新6篇) 初三学生作文 篇一:我的偶像初三学生作文 篇二:珍惜时光初三学生作文 篇三   人们都说:生于忧患,...
帅气的哥哥作文(精彩3篇) 帅气的哥哥作文 篇一我有一个非常帅气的哥哥。他高高的个子,脸上总是带着阳光的笑容,让人感到温暖和舒适...
追梦初三作文600字(精彩3... 追梦初三作文600字 篇一 追梦初三作文600字 篇二追梦初三作文600字 篇三追梦初三作文600字...
初三作文寻找快乐【推荐6篇】 初三作文寻找快乐 篇一快乐是什么?怎样才能找到快乐?这是每个人都在探索的问题。尤其是对于初三的学生来...
3声谢谢初三作文【最新3篇】 3声谢谢初三作文 篇一初三这三年的时光,仿佛是在一瞬间匆匆而过。回首往事,我不禁感慨万分,心中涌动着...
什么是友情初三作文(实用3篇... 什么是友情初三作文 篇一友情是一种宝贵的情感,它是人们在成长过程中所建立起来的一种特殊的关系。友情不...