Unity Decal 贴花效果测试

article/2025/8/29 2:03:22

贴花效果,就和名字的直接意思类似,把一张图贴到另一个物体上显示,经常被用于表现一些重复出现的图案,比如弹孔,涂鸦,污渍等。效果图:
在这里插入图片描述

常规贴花实现

Unity官方提供了一个工程,这个工程主要是用来说明CommandBuffer是怎么使用的,其中有贴花的一些展示,主要是用CommandBuffer在Deferred渲染路径下实现贴花效果。使用CommandBuffer是因为需要把BuiltinRenderTextureType.GBuffer2中存储的法线信息传给Shader,而这次测试主要为了验证原理,不使用法线信息,所以可以不用CommandBuffer(即使使用法线信息也可以在Shader中通过_CameraDepthNormalsTexture结合DecodeDepthNormal方法来获取到法线信息)。原工程通过 cam.AddCommandBuffer (CameraEvent.BeforeLighting, buf); 这句实现把CommandBuffer插入到延迟渲染的光照计算Pass前面,也可以去掉不用。所以原工程的C#代码基本可以不使用,在Forward渲染路径下,完全在Shader中实现贴花效果。之前看文档说如果Shader中使用深度图的话需要在C#代码中设置相机的depthTextureMode,即 mainCam.depthTextureMode = DepthTextureMode.Depth;,但是我试了下不写这行代码在Shader中也可以正常使用深度图,有知道原因的同学可以告诉我下哈。

贴花效果的原理是建立一个立方体物体作为贴花物体(也有使用球体的),在贴花物体和被贴花的物体相交的XZ平面计算UV,显示贴花图案。具体的逻辑如下:

  1. 在顶点着色器中记录顶点在视空间的坐标(即相机到该点的方向向量,因为相机在视空间的原点)
  2. 在片元着色器中根据远裁切平面的距离(_ProjectionParams.z)和深度值重建片元在视空间的坐标
  3. 再根据unity_CameraToWorld和unity_WorldToObject矩阵计算出片元在模型空间的坐标
  4. 把模型空间坐标的xz分量映射到 [0,1] 区间,作为UV去读取贴花图案

直接上代码:

Shader "MJ/ForwardDecal"
{Properties{_MainTex ("Decal Texture", 2D) = "white" {}}SubShader{Tags{ "Queue"="Geometry+1" }Pass{ZWrite OffBlend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma target 3.0#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float4 pos : SV_POSITION;float4 screenUV : TEXCOORD0;float3 ray : TEXCOORD1;};v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos (v.vertex);o.screenUV = ComputeScreenPos (o.pos);o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1,-1,1);return o;}sampler2D _MainTex;sampler2D _CameraDepthTexture;float4 frag(v2f i) : SV_Target{i.ray = i.ray * (_ProjectionParams.z / i.ray.z);float2 uv = i.screenUV.xy / i.screenUV.w;float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);// 要转换成线性的深度值 //depth = Linear01Depth (depth);float4 vpos = float4(i.ray * depth,1);float3 wpos = mul (unity_CameraToWorld, vpos).xyz;float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;clip (float3(0.5,0.5,0.5) - abs(opos.xyz));// 转换到 [0,1] 区间 //float2 texUV = opos.xz + 0.5;float4 col = tex2D (_MainTex, texUV);return col;}ENDCG}}Fallback Off
}

代码中需要注意的一些地方:

  1. 计算视空间方向时要乘以 float3(-1,-1,1),这一点没有想明白,试了其他值效果不对
  2. i.ray = i.ray * (_ProjectionParams.z / i.ray.z); 是为了求在当前ray方向上延伸到摄像机远平面位置的向量,这张图能清晰地说明问题,图片出自 这篇文章
    在这里插入图片描述
  3. o.screenUV = ComputeScreenPos (o.pos); 计算结果的xy分量到片元着色器中需要除以w分量才能使用,除以w后xy分量在[0,1]区间,用来作为UV去读取_CameraDepthTexture。为什么在frag除以w可以参考 文章
  4. clip (float3(0.5,0.5,0.5) - abs(opos.xyz)) 的意思是剔除在物体外的片元,opos为转换到模型空间下的坐标,该模型是一个立方体,其模型空间坐标范围是 [-0.5, 0.5]。
  5. depth = Linear01Depth (depth); 是为了得到线性的深度值,为了 float4 vpos = float4(i.ray * depth,1) 计算时能够得到正确的向量。SAMPLE_DEPTH_TEXTURE 方法取得的深度值是非线性的。参考文章。
  6. float2 texUV = opos.xz + 0.5; 把坐标映射到 [0, 1] 区间,这里使用xz坐标,因为贴花要显示在xz平面上。

效果图:
在这里插入图片描述

考虑y方向偏移的贴花

在摆弄贴花物体时发现在拐角和边缘处显示效果不对,出现图片边缘被clamp的效果,如图:
在这里插入图片描述
原因在于在计算纹理坐标时 (float2 texUV = opos.xz + 0.5;)没有考虑y方向的变化,导致在边缘处的片元xz坐标都一样,和clamp对纹理坐标的处理一样。这种情况可以通过把贴花旋转一定的角度来消除,像这样(x轴旋转了-50度):在这里插入图片描述

也可以通过使用法线图来确定模型空间坐标在y方向上的偏差,使这部分偏差参与到UV的计算中,主要步骤有:

  1. 求出视空间的深度值和法线(通过_CameraDepthNormalsTexture属性和DecodeDepthNormal方法)
  2. 把法线转换到模型空间
  3. 把模型空间法线和模型空间Up方向(float3(0,1,0))点乘,求出垂直方向上的偏移程度,
  4. 把偏移程度加入到UV的计算中

Shader代码:

Shader "MJ/ForwardDecal_YOffset"
{Properties{_MainTex ("Decal Texture", 2D) = "white" {}}SubShader{Tags{ "Queue"="Geometry+1" }Pass{ZWrite OffBlend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma target 3.0#pragma vertex vert#pragma fragment frag2#include "UnityCG.cginc"struct v2f{float4 pos : SV_POSITION;float4 screenUV : TEXCOORD1;float3 ray : TEXCOORD2;float3 orientation : TEXCOORD3;float3 worldNormal : TEXCOORD4;};v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos (v.vertex);o.screenUV = ComputeScreenPos (o.pos);o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1,-1,1);return o;}sampler2D _MainTex;sampler2D _CameraDepthNormalsTexture;float4 frag2(v2f i) : SV_Target{i.ray = i.ray * (_ProjectionParams.z / i.ray.z);float2 uv = i.screenUV.xy / i.screenUV.w;float depth;float3 viewNormal;float4 encode = tex2D(_CameraDepthNormalsTexture, uv);// 返回一个视空间深度值 和 一个视空间法线 //// 该深度值是一个线性深度值, 范围是0到1, 精度小于使用SAMPLE_DEPTH_TEXTURE方法获得的深度值 //// 因为DecodeDepthNormal方法中的深度值用16位存储,而SAMPLE_DEPTH_TEXTURE中的深度值用32位存储 //DecodeDepthNormal(encode, depth, viewNormal);viewNormal = normalize(viewNormal);float4 vpos = float4(i.ray * depth,1);float3 wpos = mul (unity_CameraToWorld, vpos).xyz;float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;clip (float3(0.5,0.5,0.5) - abs(opos.xyz));float3 objectNormal = mul(UNITY_MATRIX_T_MV, viewNormal);objectNormal = normalize(objectNormal);float3 upDir = float3(0,1,0);float NDotU = dot(objectNormal, upDir);// float offsetScale = sin(acos(NDotU));float offsetScale = 1 - NDotU;				// 效果一样,计算更少 //// 先只考虑XZ平面 //float2 texUV = opos.xz + float2(0.5, 0.5) + offsetScale * float2(0, opos.y);float4 col = tex2D (_MainTex, texUV);return col;}ENDCG}}Fallback Off
}

C#中需要加上 mainCam.depthTextureMode = DepthTextureMode.DepthNormals;, 这样可以在Shader中使用 _CameraDepthNormalsTexture 属性,结合 DecodeDepthNormal 方法可以获取到视空间中的深度值和法线。官方文档。

效果图:
在这里插入图片描述
效果图中Scene窗口部分的贴花看起来很扭曲,非常不对,Game窗口的部分变化不大,但是也能看到有一些锯齿存在,关于这个问题我查了一些文章,大概猜测是深度值精度问题导致,_CameraDepthNormalsTexture 中的RG通道用来存储法线信息(16位),BA通道用来存储深度值(16位),而 _CameraDepthTexture 中32位都用来存储深度,所以通过 _CameraDepthTexture 读取的深度值比通过 _CameraDepthNormalsTexture 读取的精确度更高。

但是y方向偏移的方法也有一些问题,比如要通过贴花物体的y坐标来控制垂直部分纹理显示的多少,还有在计算decalUV时要考虑到X和Z两个方向坐标对y偏移的计算,上述例子的代码中为了简便只给Z方向上考虑了Y的偏移,可以在Shader中设置一个Enum,在场景同学摆放贴花时根据摆放位置来控制具体在哪个方向上考虑Y偏移,那么这样一来其实也可以直接用第一种常规方式来实现,反正都需要人工干预,而且第一种方式还少进行了一次矩阵乘法和点乘。

总结:
鉴于贴花在物体拐角和边缘处表现的不是很好,建议的使用方式是在离线时布置好贴花的,这样可以根据不同物体的旋转缩放等条件来调整贴花物体,来达到良好的表现。尽量避免在运行时动态生成,或者只在有限的场景条件里动态生成,比如平地,墙面之类,以减少不确定性以及避免出现预期以外的奇怪效果。

参考链接:
https://blog.csdn.net/NotMz/article/details/78712346
https://forum.unity.com/threads/decodedepthnormal-linear01depth-lineareyedepth-explanations.608452/
https://docs.unity3d.com/Manual/SL-CameraDepthTexture.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html
https://docs.unity3d.com/Manual/SL-DepthTextures.html

http://chatgpt.dhexx.cn/article/OhIVZjXi.shtml

相关文章

UE4 Decal 贴花不在静态光照下绘制

Decal顶点没有烘焙的光照数据,因此无法在含前向管线的阴影下绘制。特效贴花为unlit自发光材质,阴影下表现影响不大。而场景Decal需要计算光照。 1. Decal实现原理 MobileDecalRendering.cpp 通过矩阵变换 得到FrustumComponentToClip,传入…

游戏中的Decal(贴花)

在游戏中,decal是一种非常常见的效果,常用来实现弹孔,血迹,涂鸦等效果。最近研究了下Decal在游戏引擎中的实现方式,大致总结了一下:1.基于面片实现:直接用一个Quat的mesh,加上一张贴图&#xff…

UE4 Decal实现简介

Decal 是游戏中常见的一个东西,经常被用在 显示弹痕,地面叠加花纹等。 Decal 绘制流程 Decal可以认为是,将一个面的画面沿Decal的Box的X轴方向投影到物体表面。 Decal的绘制实际只有一个Box绘制。 RHICmdList.DrawIndexedPrimitive(GetUn…

跳出for循环

跳出for循环有三种方式&#xff1a; 1&#xff1a;continue&#xff1b;跳出当次循环&#xff0c;可继续进行下一个循环&#xff1b; function ceshi(){for(var i 0 ; i < 6 ; i){if(i 3){continue;}console.log(,i);} }ceshi(); 效果图&#xff1a; 2&#xff1a;brea…

跳出forEach循环

我们平时用到的循环有很多种。for, map, while, forEach, for...of, for...in等等&#xff0c;每种循环都有在某一次循环语句中跳出本次循环的方法&#xff0c;但是除了forEach。 有小伙伴说不用不就好了。其实这些循环里边&#xff0c;当属for的效率最高&#xff0c;for...in最…

Java 8 跳出foreach循环,跳出本次循环,继续执行,之前的for each循环如何跳出本次循环,跳出循环,跳出多层for循环。

在Java8之前&#xff0c;最开始使用for i 循环&#xff0c;很老旧&#xff0c; 后来有了高级的for each 循环&#xff0c;然后这个跳出本次循环和跳出所有的for循环&#xff0c;都简单&#xff0c;稍微没见过的就是跳出多层for循环。 然后就是Java8出的foreach循环&#xff0…

js中的for循环如何跳出,js中for循环的两种语法

js几种for循环的几种用法 谷歌人工智能写作项目&#xff1a;小发猫 js&#xff0c;for循环是怎么运行的&#xff1f; typescript有哪些变化。 最普遍的介绍&#xff1a;for循环是JavaScript中最常用的循环&#xff0c;标准for循环代码格式为&#xff1a;for(定义变量初始值;…

if/while/do-while/for循环以及跳出循环break/return/continue

流程控制对任何一门编程语言都是至关重要的&#xff0c;它为我们提供了控制程序步骤的基本手段。常见对主要分为&#xff0c;条件语句、循环语句、跳转语句。 1、if语句 if 语句是一种判断语句。 语法&#xff1a; if(条件){条件成立时执行的代码 } if...else 语句当条件成…

Python中跳出循环的两种方法

我们经常遇到循环在进行到某一个特定的值时&#xff0c;需要跳出循环&#xff0c;或跳过这个值&#xff0c;python中早已为我们准备了这样的参数:break,continue 比如下面的for循环&#xff1a; for i in range(1,10):print(循环了,i,次) 结果显而易见如图&#xff1a; 当我…

js foreach与for循环之return跳出循环

因为自己比较大只&#xff0c;容易忘记&#xff0c;仅此用来记录一下~ 各种循环中使用return或者退出循环的机制。 1、forEach 使用 return 可以退出循环吗&#xff1f;下面代码打印啥&#xff1f; const list [1, 2, 3, 4, 5]list.forEach(e > {if (e 3) {return}consol…

JS中如何跳出.forEach循环

写在前面 提到在一段程序中如果碰到需要终止&#xff0c;结束一个循环&#xff0c;函数或者一段代码&#xff0c;一般会想到以下这几个关键字return、continue、break 简述一下三者的区别&#xff1a; break: 终止整个循环(有内层循环时终止的是内层循环)&#xff0c;退出swi…

JavaScript foreach 方法跳出循环

通常&#xff0c;在 for循环中跳出循环可以用 break或者 continue 来跳出循环。 break&#xff1a;跳出循环&#xff1b; continue&#xff1a;跳过当次循环。 而有时候需要在 foreach 中跳出循环&#xff0c;该怎么做呢&#xff1f; forEach() 方法用于调用数组的每个元素&am…

C++跳出for循环的方式

注&#xff1a;continue只能跳出当前层的循环&#xff0c;无法直接跳出for循环 方法1&#xff1a; for循环中满足条件后使用break语句&#xff1b; #include <iostream> using namespace std;int main(int argc,char *argv[]) {int i;for(i0;i<5;i){if(i2)break;}cou…

Js之跳出for循环,跳出多次for循环详解

一、for循环退出方式 首先我们都知道循环中最常用的就是continue;break; continue:表示跳出本次循环&#xff0c;也就是不执行本次循环continue之后的操作 break:表示跳出当前的循环&#xff0c;针对整个循环体终止后续的遍历&#xff1b; 最简单的应用效果如下&#xff1a; …

layui遮罩

layui遮罩 在执行某个按钮功能时,可能出现网络延迟或是其他原因时 避免重复表单提交,用遮罩.此次请求不响应,执行完毕,就一直遮罩住其他功能 var loading layer.load(1, {shade: [0.8, #393D49], time: 0}); layer.close(loading);写的位置 在请求开启之前遮住 在请求响应之…

遮罩Mask

一、遮罩&#xff1a;遮罩层范围内可看见被遮罩层图层&#xff0c;遮挡层范围可看见笼罩层图形 遮罩层必须至少有两个图层&#xff0c;上面的一个图层为“遮罩层”&#xff0c;下面的称“被遮罩层”&#xff1b;这两个图层中只有相重叠的地方才会被显示。也就是说在遮罩层中有…

基于vue3.0的遮罩

前言 最近在uniapp中要写一个弹窗&#xff0c;弹窗好写&#xff0c;但是遮罩没有接触过。找到了下面这篇文章&#xff0c;推荐仔细阅读一下。在这篇文章的基础下&#xff0c;简单封装了一个遮罩。 推荐文章&#xff1a;论如何用Vue实现一个弹窗-一个简单的组件实现 遮罩 备…

html5 div遮罩,CSS3 遮罩属性

遮罩(Mask)和Photoshop里的蒙版效果含义一致&#xff0c;用于为指定元素建立一个覆盖在上面的遮罩层。它显示在元素之上&#xff0c;和背景效果使用起来正好相反。我们可以使用遮罩在元素上方叠加一幅图或者一个渐变效果。 遮罩、内容和背景效果的显示层次如下图所示&#xff1…

html添加遮罩

html添加遮罩的代码如下所示 使用divcss &#xff0c;加载中的图片是网上下载的动图&#xff0c;可以根据自己需要进行修改 <!DOCTYPE html> <html> <head><title>DIV CSS遮罩层</title><script language"javascript" type"t…

Android渐变遮罩

Android Studio版本&#xff1a; Android Studio Chipmunk 这篇文章介绍如何在Android中实现这样的渐变遮罩 依赖项 1.CardView 在app下的build.gradle中添加依赖&#xff0c;并且完成编译 dependencies {implementation androidx.cardview:cardview:1.0.0 } 2.两张svg图片…