Unity地面交互效果——1、局部UV采样和混合轨迹
大家好,我是阿赵。
这期开始,打算介绍一下地面交互的一些做法。
比如:
Unity引擎制作沙地实时凹陷网格的脚印效果
或者:
Unity引擎制作雪地效果
这些效果的实现,需要基于一些基础的知识。所以这一篇先介绍一下简单的局部UV采样,然后映射纹理到地面的做法。
大概需要实现的效果是这个视频的前半部分:
Unity曲面细分制作雪地效果
一、轨迹的绘制
看这段视频的前半部分。可以看到,球在移动的过程中,在地面产生了移动的轨迹

这个效果可能很多朋友都会做,一般的做法是计算球的坐标相对于整个地面的位置,然后拾像素绘制在地面的遮罩贴图上面。
不过这种做法会有一个问题,假如地面很大的时候,通过一张和整个地面匹配UV的遮罩贴图来绘制轨迹,那么这张遮罩贴图的分辨率需要多大,才能显示足够的精度呢?比如一个4096米4096米的地面,就算我们用一张40964096的贴图做遮罩,那么每平方米的面积,才占一个像素,明显是绘制不出这么清晰的轨迹图形的。
其实我们没有必要去绘制整张贴图,只需要局部绘制就好了


绘制这一个小局部,然后通过局部UV采样的方式,把这个贴图叠加到大贴图上面去。
这时候,就需要给Shader传入一个范围,让Shader知道,这个局部UV,最终占整个地面UV的多少。
地面的Shader代码是这样的:
Shader "azhao/GroundFootStep"
{Properties{_MainTex("Texture", 2D) = "white" {}_Color("Color", Color) = (1,1,1,1)_centerPos("CenterPos", Vector) = (0,0,0,0)_footstepRect("footstepRect",Vector) = (0,0,0,0)_footstepTex("footstepTex",2D) = "gray"{}_footstepColor("footstepColor",Color) = (1,1,1,1)}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _MainTex;float4 _MainTex_ST;fixed4 _Color;uniform float3 _centerPos;float4 _footstepRect;sampler2D _footstepTex;float4 _footstepColor;struct appdata{float4 pos : POSITION;float2 uv : TEXCOORD0;};struct v2f{float4 pos : SV_POSITION;float3 worldPos : TEXCOORD0;float2 uv : TEXCOORD1;float2 footstepUV : TEXCOORD2;};float RemapUV(float min, float max, float val){return (val - min) / (max - min);}v2f vert(appdata i){v2f o;o.pos = UnityObjectToClipPos(i.pos);o.worldPos = mul(unity_ObjectToWorld,i.pos.xyz);o.uv = i.uv*_MainTex_ST.xy+ _MainTex_ST.zw;o.footstepUV = float2(RemapUV(_footstepRect.x, _footstepRect.z, o.worldPos.x), RemapUV(_footstepRect.y, _footstepRect.w, o.worldPos.z));return o;}fixed4 frag (v2f i) : SV_Target{// sample the texturefixed4 col = tex2D(_MainTex, i.uv)*_Color;fixed4 footstepCol = tex2D(_footstepTex, i.footstepUV);fixed3 footstepRGB = _footstepColor.rgb;fixed3 finalRGB = col.rgb*(1 - footstepCol.a) + footstepRGB * footstepCol.a;fixed4 finalCol = fixed4(saturate(finalRGB), 1);return finalCol;return col;}ENDCG}}
}
从代码可以看出footstepRect是一个很关键的东西,它告诉了Shader,需要绘制轨迹的范围在哪里。然后通过RemapUV方法,拿这个范围和当前的顶点世界坐标去计算出,当前的点该占整体UV的实际位置。
这个footstepRect其实是C#动态算出来的,根据角色所在的坐标和半径,算出来一个范围。
C#的代码大概是这样:
Vector3 pos = role.transform.position;
mat.SetVector("_centerPos", pos);
mat.SetFloat("_maxVal", radius);
mat.SetVector("_footstepRect", new Vector4(pos.x - radius, pos.z - radius, pos.x + radius, pos.z + radius));
其实就是中心点加减半径而已。
这个做法的优点是,只需要局部绘制一张贴图,就能达到比较清晰的轨迹图形
缺点是,只能在一定范围内显示,超出了footstepRect范围,轨迹就会消失了。
二、绘制轨迹的手段
绘制轨迹,其实就是连贯的把某个笔刷的像素复制到一张图片上。这个应该不是很难理解的概念。
上面的例子,球是一个笔刷,它移动的时候,它所在的位置会产生一个圆形的笔刷,通过连续每帧的覆盖,就形成了一个轨迹。

如果绘制的间隔拉大一点,看到的情况大概是这样的。
那么问题来了,球移动的时候,上面说到,相对于地表贴图的footstepRect,是会变化的,所以说,我们不能直接把球的笔刷印到之前的那张图去。
比如上一张图的位置是在这里

下一张图的位置就变成了这里

留意看左下角的球,它在世界中的位置是一直没有变化的,但在这个footstepRect的局部里面,它的相对位置是变化了的。
下面来说一下具体的做法。
1、通过摄像机绘制RenderTexture

这里为了渲染一张顶视图,我是打了一个摄像机在运动的球的上方,然后摄像机跟随这球移动。
需要注意的是,摄像机一定要是正交的,然后通过控制orthographicSize参数,可以准确的绘制符合footstepRect的范围。最后,给这个摄像机的targetTexture赋予一张RenderTexture,作为输出。
2、通过偏移来叠加上一张图
刚才那个RenderTexture是每帧都会渲染一次的。我们需要2张RenderTexture,一张是上一次留下的,一张是这一帧渲染出来的。
接下来就是把两张RenderTexture,通过Graphics.Blit方法合并在一起。由于Graphics.Blit方法是可以传入一个材质球的,所以可以通过写一个Shader来混合2张贴图。具体的方式是,计算上一帧和当前帧角色所在位置的偏移,然后用偏移来控制上一帧的贴图的UV采样,再把两张贴图合并在一起就可以了。
3、合并的Shader
Shader "azhao/DrawFootstep"
{Properties{_MainTex ("Texture", 2D) = "white" {}_lastTex("lastTex",2D) = "black"{}_offset("offset",Vector) = (0,0,0,0)}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;sampler2D _lastTex;float2 _offset;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}half4 frag (v2f i) : SV_Target{// sample the texturehalf4 col = saturate(tex2D(_MainTex, i.uv));half3 curRGB = col.rgb * 2 - 1;half4 lastCol = saturate(tex2D(_lastTex, i.uv - _offset));float lastAlpha = lastCol.a;half3 lastRGB = lastCol.rgb*2-1;half mr = lastRGB.r*lastAlpha;if (col.a >0){if (curRGB.r > 0){if (lastAlpha == 0){mr = curRGB.r;}}else if (curRGB.r < 0){mr = min(curRGB.r,mr);}}else{mr = lastRGB.r;}mr = (mr + 1) / 2;float alpha = max(col.a, lastAlpha);half3 mixRGB = half3(mr, mr, mr);half3 finalRGB = mixRGB;return half4(finalRGB, alpha);}ENDCG}}
}
三、细节问题
第一步绘制轨迹通过局部UV坐标采样,和地表的贴图纹理混合。这里会存在一个问题。通过第二步绘制出来的轨迹贴图,是Clamp平铺方式的

这意味着,超出了UV的0到1范围的坐标,会直接采样了0或者1的UV。具体的表现是这样的:

这个黑线,其实就是到边缘了,所以超出的部分,都会是黑的

为了解决这个问题,可以加一个渐变的遮罩叠加

把UV接近0和1的地方都变成纯黑色,这样就不会出现Clamp平铺的问题,也可以让接近边缘的地方不会有一个很硬的消失,而是稍微柔软的过渡。


所以用于绘制轨迹混合的shader会变成这样:
Shader "azhao/DrawFootstep"
{Properties{_MainTex ("Texture", 2D) = "white" {}_lastTex("lastTex",2D) = "black"{}_offset("offset",Vector) = (0,0,0,0)_maskTex("maskTex",2D) = "white"{}_reduceVal("reduceVal",float) = 0.001}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;sampler2D _lastTex;float2 _offset;sampler2D _maskTex;float _reduceVal;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);return o;}half4 frag (v2f i) : SV_Target{// sample the texturehalf4 col = saturate(tex2D(_MainTex, i.uv));half3 curRGB = col.rgb * 2 - 1;half4 lastCol = saturate(tex2D(_lastTex, i.uv - _offset));float lastAlpha = saturate(lastCol.a - _reduceVal);half4 maskCol = tex2D(_maskTex, i.uv);half3 lastRGB = lastCol.rgb*2-1;half mr = lastRGB.r*lastAlpha;if (col.a >0){if (curRGB.r > 0){if (lastAlpha == 0){mr = curRGB.r;}}else if (curRGB.r < 0){mr = min(curRGB.r,mr);}}else{mr = lastRGB.r;}mr = (mr + 1) / 2;float alpha = max(col.a, lastAlpha)*maskCol.r;half3 mixRGB = half3(mr, mr, mr);half3 finalRGB = mixRGB * maskCol.rgb;return half4(finalRGB, alpha);}ENDCG}}
}
相关文章:
Unity地面交互效果——1、局部UV采样和混合轨迹
大家好,我是阿赵。 这期开始,打算介绍一下地面交互的一些做法。 比如: Unity引擎制作沙地实时凹陷网格的脚印效果 或者: Unity引擎制作雪地效果 这些效果的实现,需要基于一些基础的知识。所以这一篇先介绍一下简单…...
基于STM32的示波器信号发生器设计
**单片机设计介绍,基于STM32的示波器信号发生器设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序文档 六、 文章目录 一 概要 基于STM32的示波器信号发生器是一种高性能的电子仪器,用于测试和分析电路中的电信号。在该系统中&a…...
案例分析大汇总
案例分析心得 2018-2022年的案例分析考试内容汇总(近五年) 架构设计题型 软件系统建模 数据库 Web 系统设计 2018年 胖/瘦客户端 C/S 架构非功能性需求 数据流图DFDE-R图Essential Use Cases(抽象用例),Real Use Cases(基础用例)信息工…...
MVCC(Multi-Version Concurrency Control,多版本并发控制)
是一种数据库管理系统中常用的并发控制技术,用于处理多个事务同时访问数据库数据时的数据一致性和隔离性。MVCC的主要目标是允许多个事务并发执行,同时保持数据的一致性,避免数据丢失或不一致。 MVCC 的核心思想是为每个事务维护多个版本的数…...
嵌入式面试2(c相关)
目录 1.C语言中static、const、volatile关键字用法区别; static的用法(定义和用途) const的用法(定义和用途) volatile (英文意思为易变的) 作用和用法: 2.C语言中,const 和 static 的区别,c…...
基于SSM的n省出口基地公共信息服务平台设计与实现
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…...
opencv dnn模块 示例(20) 目标检测 object_detection 之 yolor
文章目录 1、论文介绍1.1、YOLOR思想动机1.2、隐式知识学习1.2.1、隐式知识如何工作1.2.2、隐式知识统一网络建模 1.3、实验1.4、总结 2、测试2.1、opencv dnn2.1.1、代码2.1.2、结果 2.2、测试效率 YOLOR出自论文You Only Learn One Representation: Unified Network for Mult…...
【队列的顺序表示,链式表示】
文章目录 队列的表示和实现相关术语队列的表示链队的表示链队的定义链队的初始化销毁链队列 链队列的入队出栈 队列的表示和实现 相关术语 队列(Queue)是仅在表尾进行插入操作,在表头进行删除操作的线性表。表尾即an端,称为队尾…...
Pydantic 实践
1. 简介 pydantic 库是一种常用的用于数据接口 schema 定义与检查的库。 通过 pydantic 库,我们可以更为规范地定义和使用数据接口,这对于大型项目的开发将会更为友好。 当然,除了 pydantic 库之外,像是 valideer 库、marshmallo…...
获取pandas中的众数
pandas.DataFrame 也有一个 mode() 方法。 以下面的 pandas.DataFrame 为例。 df pd.DataFrame({‘col1’: [‘X’, ‘X’, ‘Y’, ‘X’], ‘col2’: [‘X’, ‘Y’, ‘Y’, ‘X’]}, index[‘row1’, ‘row2’, ‘row3’, ‘row4’]) print(df) col1 col2 row1 X X row2…...
SOLIDWORKS Simulation2024仿真10大新功能
SOLIDWORKS Simulation新增功能 1. 增强型轴承接头 •通过指定压缩、拉伸和弯曲的刚度,轻松创建自定义轴承接头。•通过向非线性和大型位移算例添加自定义条件,提高模拟精度。 优点:使用功能强大的接口,更轻松 、 更 准 确 地 设…...
Java程序设计2023-第二次上机练习
这里要用到一些面向对象的基本知识 目录 7-1 伪随机数 输入格式: 输出格式: 输入样例: 输出样例: 7-2 jmu-Java-03面向对象基础-01-构造方法与toString 1.编写无参构造函数: 2.编写有参构造函数 3.覆盖toString函数: 4.对每个属性生成setter…...
如何在 uniapp 里面使用 pinia 数据持久化 (pinia-plugin-persistedstate)
想要在 uniapp 里面使用 pinia-plugin-persistedstate 会遇到的问题就是 uniapp里面没有浏览器里面的 sessionStorage localStorage 这些 api。 我们只需要替换掉 pinia-plugin-persistedstate 默认的储存 api 就可以了。使用 createPersistedState 重新创建一个实例, 把里面的…...
智慧矿山AI算法助力护帮板支护监测,提升安全与效率
在智慧矿山AI算法系列中,护帮板支护监测是保障矿山安全和提高生产效率的重要环节。护帮板作为矿山支护体系中的重要组成部分,在矿山生产中起到了关键的作用。那么,护帮板在哪种状态下是正常打开的呢?本文将对此进行介绍。 护帮板的…...
shell中的运算
目录 1.运算符号 2.运算指令 练习 1.运算符号 运算符号意义加法-减法*乘法/除法%除法后的余数**乘方自加一- -自减一<小于<小于等于>大于>大于等于等于ji ->jji*j*i->jj*i/j/i->jj/i%j%i->jj%i 2.运算指令 (()) //((a12))let //let a12 …...
【Java 进阶篇】解决Java Web应用中请求参数中文乱码问题
在Java Web应用开发中,处理请求参数时经常会遇到中文乱码的问题。当浏览器向服务器发送包含中文字符的请求参数时,如果不正确处理,可能会导致乱码问题,使得参数无法正确解析和显示。本文将详细探讨Java Web应用中请求参数中文乱码…...
51单片机-点阵屏led
代码配置 这样就能选择每一列哪个亮了 进行位选,段清零,这样就不会影响多列同时了 实现动画 1、使用文字摸提取文件,提取图案的16进制表示数组 offest作为偏移量,count作为计时。count10,偏移量加1,就相当于得到下一…...
Angular-03:组件模板
各种学习后的知识点整理归纳,非原创! 组件模板 ① 数据绑定② 属性绑定③ 类名绑定④ 样式绑定⑤ 事件绑定⑥ 获取原生DOM对象6.1 在组件模板中获取6.2 在组件类中获取 ⑦ 双向数据绑定⑧ 内容投影8.1 select选择器8.2 单槽投影8.3 多槽投影 ⑨ 安全操作…...
mysql 操作慢查询日志
1、mysql 批量插入300w数据 CREATE PROCEDURE test_insert_200w() BEGINDECLARE i INT;SET i1;WHILE i<3000000 DOINSERT INTO shop_user (password, telephone, username) VALUES (admin, 15510304125, concat(admin, i));SET ii1;END WHILE; END; //执行sql call test_in…...
illuminate/database 使用 二
上一篇文章写怎么单独使用illuminate/database,这回讲下怎么整合到项目里使用。为此特意看了下laravel对其使用。本篇文章,参照laravel的使用,简单实现。 一 原理 laravel 里使用illuminate/config。 illuminate/config composer 地址&…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
Python实现简单音频数据压缩与解压算法
Python实现简单音频数据压缩与解压算法 引言 在音频数据处理中,压缩算法是降低存储成本和传输效率的关键技术。Python作为一门灵活且功能强大的编程语言,提供了丰富的库和工具来实现音频数据的压缩与解压。本文将通过一个简单的音频数据压缩与解压算法…...
