Unity地面交互效果——2、动态法线贴图实现轨迹效果

Unity引擎动态法线贴图制作球滚动轨迹

  大家好,我是阿赵。
  之前说了一个使用局部UV采样来实现轨迹的方法。这一篇在之前的基础上,使用法线贴图进行凹凸轨迹的绘制。

一、实现的目标

  先来回顾一下,上一篇最终我们已经绘制了一个轨迹的贴图
在这里插入图片描述

  可以思考一下,假如现在我绘制的不是黑白的遮罩,而是一张法线贴图,会怎样呢?比如这样:
在这里插入图片描述

  如果是这样,剩下的问题就非常简单了,使用局部的UV采样,然后正常的通过法线贴图读取法线方向,最后通过光照模型来表现出法线的凹凸感。
在这里插入图片描述

二、动态法线贴图的绘制和融合

  说起来好像很简单,但绘制灰度图简单,绘制法线贴图应该怎样做呢?
在这里插入图片描述

  在球的位置上,实际上我是用了这么一张法线贴图的,还是用顶部摄像机拍摄需要局部采样UV的范围。
  在球没有移动的时候,顶部摄像机拍摄到的应该是这样一个情况:
在这里插入图片描述

  除了球以外,这一片平整的纯色,是算出来的。
  这里涉及到一些法线贴图的计算方式。
  需要知道的是,法线贴图是一张RGB颜色的贴图,所以它的每一个通道的取值范围是0到1,但法线方向的取值范围是-1到1的,所以如果得到一张法线贴图,需要转换成法线方向时,需要对它乘以2再减1。然后想将一个法线方向转成RGB值,方法是把法线方向乘以0.5再加0.5。
  所以当一个正常朝向上方的法线方向是(0,0,1),它转换成RGB值之后,就是(0.5,0.5,1),也就是上图看到占了大部分面积的颜色了。
  由于我使用的是打一个正交摄像机在头顶的做法来渲染球所在的区域,这个摄像机通过layer过滤只看得到球所在的法线面片,所以除了球以外,其他区域应该是黑色的:
在这里插入图片描述

  想要把黑色的部分都填充成(0,0,1)法线方向的颜色,有2种方法:
1、把摄像机的颜色设置成这种颜色
在这里插入图片描述

2、在混合的shader里面判断,传入的图片是黑色的部分就填充成这张颜色。
  我这里是简单的使用第一种方式,修改摄像机的颜色。
  用上一篇说的方法,在移动的过程中,通过求出上一帧球的位置和当前球的位置作为偏移值,然后传入通过Graphics.Blit融合两帧的画面的材质球里面,就可以对两张法线贴图进行混合了。
  接下来需要混合两帧之间产生的法线贴图。
  混合法线的方法非常多,我常用的消耗比较低的法线混合方式有这么几种:
  假设n1和n2是需要混合的2个法线方向,n3是混合后的结果
1、线性混合
公式很简单:

float3 n3 = normalize(n1+n2);

在这里插入图片描述

这种方式混合法线,优点是实现简单,而且同一个法线贴图不停的叠加,效果都不会发生变化。缺点也很明显,会减淡本身法线贴图的特征,如果每帧都不停的和(0,0,1)混合,最终所有法线的特征都会被抹平了。
2、偏导混合
公式是:

float3 n3 = normalize( float3(n1.xy/n1.z+n2.xy/n2.z,1));

在这里插入图片描述

从效果上看,法线的特征保留会比线性混合要好很多。
如果把公式改一下,

float2 n0 = lerp(n1.xy/n1.z+n2.xy/n2.z,_blendVal);
float3 n3 = normalize( float3(n0,1));

还可以做出混合插值的效果。
3、Whiteout混合
和偏导的结果比较类似
公式:

n3 = normalize( float3( n1.xy+n2.xy,n1.z*n2.z));

在这里插入图片描述

三、绘制频率问题和解决

  虽然偏导和Whiteout的效果会比线性的好,但如果物体停留在同一个位置,每帧都不停的叠加同一个法线贴图,偏导和Whiteout都会对法线效果越来越加深,到最后就会出现和原来贴图比较偏差的效果,比如刚才那个球的法线如果每一帧不停的用Whiteout算法去叠加,最后就会变成这样
在这里插入图片描述

  为了解决这个问题,实际上是需要控制绘制的次数。基本思想很简单,就是物体移动的时候,并且移动的距离大于一定长度时,才会去绘制。
  这样,就出现了多种解决的方案:
1、还是用顶部摄像机,但只有主角移动的时候,才会调用Graphics.Blit方法去混合。这个方案的问题在于,如果只判断主角移动,那么产生这个法线贴图的主体也只能是主角,方便的其他角色是不能产生法线混合效果的。
2、不用顶部摄像机,而是每个可以产生轨迹的物体身上都挂上脚本,当物体移动的时候,主动的把自己的法线贴图通过Graphics.Blit混合到局部UV的法线贴图里面。这样做相当于每个角色有一个笔刷。不过这样做的问题是,需要逐个角色分别传入混合,还要计算每个角色相对于主角位置的偏移。如果场景里面的角色很多的情况下,这样做性能可能不是特别好。
3、还是用顶部摄像机,每个角色身上有一张法线贴图,只有当角色开始移动时,这张法线贴图才会变成激活可显示状态。那么,我们还是用顶部摄像机来渲染一张RenderTexture就够了,而这张RenderTexture看到的,只会是在绘制局部UV的范围内的正在移动的物体的法线贴图。
4、不管法线叠加变形的问题。实际上如果不是对混合后的法线要求特别精确,比如角色踩出的脚印要清晰到连鞋底都看得清,而只是需要一个大致的范围的话,我觉得直接叠加也无所谓,毕竟法线的大致方向是对的。

四、源码:

1、法线混合的Shader代码

和上一篇差不多的,只是修改一下片段着色器就行

			half4 frag(v2f i) : SV_Target
			{
				//当前帧传入的法线笔刷
				half4 col = tex2D(_MainTex, i.uv);
				half3 curNormal = col.rgb * 2 - 1;
				//上一帧绘制的法线贴图
				half4 lastCol = tex2D(_lastTex, i.uv - _offset);
				half3 lastNormal = lastCol.rgb * 2 - 1;			
				//默认的法线方向
				float3 defaultNormal = float3(0,0,1);
				//用于保持边缘是默认法线方向的遮罩
				half4 maskCol = tex2D(_maskTex, i.uv);
				//使用whiteout法线混合
				float3 finalNormal = normalize(half3(curNormal.xy + lastNormal.xy, curNormal.z*lastNormal.z));
				//计算边缘遮罩
				finalNormal = normalize(finalNormal * maskCol.r + _defaultDir.xyz*(1 - maskCol.r));
				//转RGB颜色
				finalNormal = finalNormal * 0.5 + 0.5;
				return half4(finalNormal, 1);

			}

2、地面上的局部UV采样包含法线贴图的Shader

Shader "azhao/GroundFootStepNormalTex"
{
    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)
		_NormalTex("Normal Tex", 2D) = "black"{}
		_normalScale("normalScale", Range(-5 , 5)) = 0
		_normalFootStepScale("normalFootStepScale", Range(-5 , 5)) = 0
		_specColor("SpecColor",Color) = (1,1,1,1)
		_shininess("shininess", Range(1 , 100)) = 1
		_specIntensity("specIntensity",Range(0,1)) = 1
		_ambientIntensity("ambientIntensity",Range(0,1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            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;
			sampler2D _NormalTex;
			float4 _NormalTex_ST;
			float _normalScale;
			float _normalFootStepScale;
			float4 _specColor;
			float _shininess;
			float _specIntensity;
			float _ambientIntensity;
			struct appdata
			{
				float4 pos	: POSITION;
				float2 uv  : TEXCOORD0;
				float3 normal:NORMAL;
				float3 tangent:TANGENT;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldPos	: TEXCOORD0;
				float2 uv  : TEXCOORD1;
				float2 footstepUV : TEXCOORD2;
				float3 worldNormal : TEXCOORD3;
				float3 worldTangent :TEXCOORD4;
				float3 worldBitangent : TEXCOORD5;
			};
			half3 UnpackScaleNormal(half4 packednormal, half bumpScale)
			{
				half3 normal;
				//由于法线贴图代表的颜色是0到1,而法线向量的范围是-1到1
				//所以通过*2-1,把色值范围转换到-1到1
				normal = packednormal * 2 - 1;
				//对法线进行缩放
				normal.xy *= bumpScale;
				//向量标准化
				normal = normalize(normal);
				return normal;
			}
			//获取HalfLambert漫反射值
			float GetHalfLambertDiffuse(float3 worldPos, float3 worldNormal)
			{
				float3 lightDir = UnityWorldSpaceLightDir(worldPos);
				float NDotL = saturate(dot(worldNormal, lightDir));
				NDotL = NDotL * 0.5 + 0.5;
				return NDotL;
			}

			//获取BlinnPhong高光
			float GetBlinnPhongSpec(float3 worldPos, float3 worldNormal)
			{
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize((viewDir + _WorldSpaceLightPos0.xyz));
				float specDir = max(dot(normalize(worldNormal), halfDir), 0);
				float specVal = pow(specDir, _shininess);
				return specVal;
			}

			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));
				o.worldNormal = UnityObjectToWorldNormal(i.normal);
				o.worldTangent = UnityObjectToWorldDir(i.tangent);
				o.worldBitangent = cross(o.worldNormal, o.worldTangent);
				return o;
			}
			
            fixed4 frag (v2f i) : SV_Target
            {

				//采样漫反射贴图的颜色
				half4 col = tex2D(_MainTex, i.uv*_MainTex_ST.xy + _MainTex_ST.zw);
				//计算法线贴图的UV
				half2 normalUV = i.uv * _NormalTex_ST.xy + _NormalTex_ST.zw;
				//采样法线贴图的颜色
				half4 normalCol = tex2D(_NormalTex, normalUV);
				fixed4 footstepCol = tex2D(_footstepTex, i.footstepUV);

				fixed3 footstepRGB = UnpackScaleNormal(footstepCol, _normalFootStepScale).rgb;

				half3 normalVal = UnpackScaleNormal(normalCol, _normalScale).rgb;
				//normalVal = footstepRGB;
				normalVal = normalize(normalVal + footstepRGB);

				//构建TBN矩阵
				float3 tanToWorld0 = float3(i.worldTangent.x, i.worldBitangent.x, i.worldNormal.x);
				float3 tanToWorld1 = float3(i.worldTangent.y, i.worldBitangent.y, i.worldNormal.y);
				float3 tanToWorld2 = float3(i.worldTangent.z, i.worldBitangent.z, i.worldNormal.z);

				//通过切线空间的法线方向和TBN矩阵,得出法线贴图代表的物体世界空间的法线方向
				float3 worldNormal = float3(dot(tanToWorld0, normalVal), dot(tanToWorld1, normalVal), dot(tanToWorld2, normalVal));

				//用法线贴图的世界空间法线,算漫反射
				half diffuseVal = GetHalfLambertDiffuse(i.worldPos, worldNormal);
				//diffuseVal = clamp(diffuseVal, 0.5, 1);

				//用法线贴图的世界空间法线,算高光角度
				half3 specCol = _specColor * GetBlinnPhongSpec(i.worldPos, worldNormal)*_specIntensity;

				//最终颜色 = 环境色+漫反射颜色+高光颜色
				half3 finalCol = UNITY_LIGHTMODEL_AMBIENT * _ambientIntensity + saturate(col.rgb*diffuseVal) + specCol;
				return half4(finalCol, 1);
			}
            
            ENDCG
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习】快速制作图像标签数据集以及训练

快速制作图像标签数据集以及训练 制作DataSet 先从网络收集十张图片 每种十张 定义dataSet和dataloader import glob import torch from torch.utils import data from PIL import Image import numpy as np from torchvision import transforms import matplotlib.pyplot…

项目管理-组织战略类型和层次讲解

组织战略类型和层次 对于不同的组织战略可能会采用不同的项目管理形式,组织作为项目管理的载体,其战略决策对项目管理体系的架构,对组织与项目之间责权利的划分具有深远的影响,组织的战略文化也会影响到项目的组织文化氛围。因此…

c++实现观察者模式

前言 我觉得这是最有意思的模式&#xff0c;其中一个动&#xff0c;另外的自动跟着动。发布-订阅&#xff0c;我觉得很巧妙。 代码 头文件 #pragma once #include<vector> #include<string> #include<iostream>// 抽象观察者 class Aobserver { public:v…

自制目录扫描工具并由py文件转为exe可执行程序

心血来潮让ChatGPT写了一个目录扫描工具&#xff0c;然后进行了一定的修改和完善&#xff0c;可以实现对网站目录的一个简单扫描并输出扫描结果&#xff0c;主要包括存在页面、重定向页面和禁止访问页面。 虽然代码很简单&#xff0c;但是做这个东西的过程还是挺有意思的&…

NoSQL数据库使用场景以及架构介绍

文章目录 一. 什么是NoSQL&#xff1f;二. NoSQL分类三. NoSQL与关系数据库有什么区别四. NoSQL主要优势和缺点五. NoSQL体系框架 其它相关推荐&#xff1a; 系统架构之微服务架构 系统架构设计之微内核架构 鸿蒙操作系统架构 架构设计之大数据架构&#xff08;Lambda架构、Kap…

伊朗网络间谍组织针对中东金融和政府部门

导语 近日&#xff0c;以色列网络安全公司Check Point与Sygnia发现了一起针对中东金融、政府、军事和电信部门的网络间谍活动。这一活动由伊朗国家情报和安全部门&#xff08;MOIS&#xff09;支持的威胁行为者发起&#xff0c;被称为"Scarred Manticore"。该组织被认…

前端BOM、DOM

文章目录 BOM操作window对象navigator对象&#xff08;了解即可&#xff09;history对象location对象弹出框警告框确认框提示框 计时相关1.过一段时间之后触发&#xff08;一次&#xff09;2.每隔三秒时间触发一次 DOM操作HTML DOM树 查找标签直接查找间接查找 节点操作操作 获…

数据库连接池大小的调整原则

配置连接池是开发人员经常犯的错误。配置池时需要理解几个原则&#xff08;对于某些人来说可能违反直觉&#xff09;。 想象一下&#xff0c;您有一个网站&#xff0c;虽然可能不是 Facebook 规模的&#xff0c;但仍然经常有 10,000 个用户同时发出数据库请求&#xff0c;每秒…

GD32 单片机 硬件I2C死锁解决方法

死锁的复现方式 在I2C恢复函数下个断点&#xff08;检测到I2C多次超时之后&#xff0c;应该能跳转到I2C恢复函数&#xff09;使用镊子&#xff0c;将SCL与SDA短接&#xff0c;很快就能看到程序停到恢复函数的断点上&#xff0c;此时再执行恢复函数&#xff0c;看能否正常走出&…

CSS3网页布局基础

CSS布局始于第2个版本&#xff0c;CSS 2.1把布局分为3种模型&#xff1a;常规流、浮动、绝对定位。CSS 3推出更多布局方案&#xff1a;多列布局、弹性盒、模板层、网格定位、网格层、浮动盒等。本章重点介绍CSS 2.1标准的3种布局模型&#xff0c;它们获得所有浏览器的全面、一致…

「直播回放」使用 PLC + OPC + TDengine,快速搭建烟草生产监测系统

在烟草工业场景里&#xff0c;多数设备的自动控制都是通过 PLC 可编程逻辑控制器来实现的&#xff0c;PLC 再将采集的数据汇聚至 OPC 服务器。传统的 PI System、实时数据库、组态软件等与 OPC 相连&#xff0c;提供分析、可视化、报警等功能&#xff0c;这类系统存在一些问题&…

历年网规上午真题(2017年)

解析:D/C 计算机主要性能指标:时钟频率(主频)、运算速度、运算精度、内存大小、数据处理速率(PDR)等 数据库主要指标:最大并发、负载均衡能力、最大连接数等 解析:A 敏捷开发是一种应对快速变化的需求的一种软件开发方法,是一种以人为核心、迭代、循序渐进的开发方…

项目实战:编辑页面加载库存信息

1、前端编辑页面加载水果库存信息逻辑edit.js let queryString window.location.search.substring(1) if(queryString){var fid queryString.split("")[1]window.onloadfunction(){loadFruit(fid)}loadFruit function(fid){axios({method:get,url:edit,params:{fi…

【使用Python编写游戏辅助工具】第四篇:Windows窗口操作

前言 这里是【使用Python编写游戏辅助工具】的第四篇&#xff1a;Windows窗口操作。本文主要介绍使用Python来实现Windows窗口的各种操作。 Windows窗口操作是游戏辅助功能中不可或缺的一部分。 Windows窗口操作指的是与Windows操作系统中的窗口进行交互和控制的操作&#xff…

【Redis】安装(Linuxwindow)及Redis的常用命令

Redis简介 Redis是一个开源&#xff08;BSD许可&#xff09;&#xff0c;内存存储的数据结构服务器&#xff0c;可用作数据库&#xff0c;高速缓存和消息队列代理。 它支持字符串、哈希表、列表、集合、有序集合&#xff0c;位图&#xff0c;hyperloglogs等数据类型。内置复…

【Java初阶练习题】-- 循环+递归练习题

循环练习题02 打印X图形计算1/1-1/21/3-1/41/5 …… 1/99 - 1/100 的值输出一个整数的每一位如&#xff1a;123的每一位是3&#xff0c;2&#xff0c;1模拟登录使用方法求最大值求斐波那契数列的第n项。(迭代实现)求和的重载求最大值方法的重载递归求N阶乘递归求 1 2 3 ...…

C++之初始化列表详细剖析

一、初始化列表定义 初始化列表&#xff1a;以一个冒号开始&#xff0c;接着是一个以逗号分隔的数据成员列表&#xff0c;每个"成员变量"后面跟一个放在括号中的初始值或表达式。 class Date { public:Date(int year, int month, int day): _year(year), _month(mont…

华纳云:centos系统中怎么查看cpu信息?

在CentOS系统中&#xff0c;我们可以使用一些命令来查看CPU的详细信息。下面介绍几个常用的命令&#xff1a; 1. lscpu lscpu命令可以显示CPU的架构、型号、核心数、线程数、频率等信息。 # lscpu 执行以上命令后&#xff0c;会输出类似以下内容&#xff1a; 2. cat /proc/…

3D医学三维技术影像PACS系统源码

一、系统概述 3D医学影像PACS系统&#xff0c;它集影像存储服务器、影像诊断工作站及RIS报告系统于一身,主要有图像处理模块、影像数据管理模块、RIS报告模块、光盘存档模块、DICOM通讯模块、胶片打印输出等模块组成&#xff0c; 具有完善的影像数据库管理功能&#xff0c;强大…

Oil Crop Science:DAP-seq技术揭示花生中AhTWRKY24和AhTWRKY106转录因子下游调控基因

2023年6月4日&#xff0c;青岛农业大学草业学院宋辉教授课题组的研究成果&#xff0c;发表在Oil Crop Science期刊上&#xff0c;文章题目为Identification of the target genes of AhTWRKY24 and AhTWRKY106 transcription factors reveals their regulatory network in Arach…