当前位置: 首页 > news >正文

BUMP图改进凹凸贴图映射

在不改变模型实际顶点结构的前提下,通过修改光照计算或采样方式,让表面看起来有凹凸细节

法线映射 - 基础版欺骗

  • 原理直接修改法线方向

    • 模型原本的法线是插值得到的平滑法线。

    • 我们从一张特殊的纹理(法线贴图)中采样,获取一个存储在切线空间中的新法线方向。

    • 使用 TBN 矩阵 将这个切线空间法线转换到世界空间。

    • 使用这个修改后的法线代替原始法线进行光照计算(如计算 dot(N, L))。

  • 效果:通过改变光线的反射方式,模拟出凹凸感。只有光影变化,没有轮廓变化。从侧面看或物体边缘,破绽会暴露(依然是平滑的)。

  • 计算代价:最低。主要开销是一次纹理采样和一次矩阵乘法(3次点积)。

切线空间

法线的存储一般都会放在模型的切线空间中。

切线空间是以物体表面的切线,副切线和法线组成的几何空间。

当我们计算的时候需要把光照的运算都放到统一坐标系下。读取法线贴图出来的法线是切线空间法线,需要世界空间转切线空间的矩阵,或者是逆矩阵。将向量统一到同一坐标系下。

                 // 转换法线、切线和副切线到世界空间o.worldNormal = normalize(mul(unity_ObjectToWorld, float4(v.normal, 0.0)).xyz);o.worldTangent = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);o.worldBinormal = normalize(cross(o.worldNormal, o.worldTangent) * v.tangent.w);...// 从法线贴图获取切线空间法线float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uv));// 创建切线空间到世界空间的转换矩阵float3x3 tangentToWorld = float3x3(i.worldTangent,i.worldBinormal,i.worldNormal);// 将法线从切线空间转换到世界空间float3 worldNormal = normalize(mul(tangentToWorld, tangentNormal));

Shader "Custom/NormalMap" {Properties {_MainTex ("Albedo (RGB)", 2D) = "white" {}_NormalMap ("Normal Map", 2D) = "bump" {}_Glossiness ("Smoothness", Range(0,1)) = 0.5_Metallic ("Metallic", Range(0,1)) = 0.0}SubShader {Tags { "RenderType"="Opaque" }LOD 200Pass {Tags { "LightMode" = "ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"struct appdata {float4 vertex : POSITION;float3 normal : NORMAL;float4 tangent : TANGENT;float2 uv : TEXCOORD0;};struct v2f {float2 uv : TEXCOORD0;float4 pos : SV_POSITION;float3 worldPos : TEXCOORD1;float3 worldNormal : TEXCOORD2;float3 worldTangent : TEXCOORD3;float3 worldBinormal : TEXCOORD4;};sampler2D _MainTex;float4 _MainTex_ST;sampler2D _NormalMap;float _Glossiness;float _Metallic;v2f vert (appdata v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;// 转换法线、切线和副切线到世界空间o.worldNormal = normalize(mul(unity_ObjectToWorld, float4(v.normal, 0.0)).xyz);o.worldTangent = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);o.worldBinormal = normalize(cross(o.worldNormal, o.worldTangent) * v.tangent.w);return o;}fixed4 frag (v2f i) : SV_Target {// 从法线贴图获取切线空间法线float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uv));// 创建切线空间到世界空间的转换矩阵float3x3 tangentToWorld = float3x3(i.worldTangent,i.worldBinormal,i.worldNormal);// 将法线从切线空间转换到世界空间float3 worldNormal = normalize(mul(tangentToWorld, tangentNormal));// 获取世界空间中的光照和视图方向float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);// 计算漫反射光照float diffuse = max(0, dot(worldNormal, lightDir));// 计算镜面反射 (Blinn-Phong)float3 halfDir = normalize(lightDir + viewDir);float specular = pow(max(0, dot(worldNormal, halfDir)), _Glossiness * 128);// 获取主纹理颜色fixed4 albedo = tex2D(_MainTex, i.uv);// 计算最终颜色fixed4 col = albedo * _LightColor0 * diffuse;col += _LightColor0 * specular * _Metallic;col.rgb += UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb;return col;}ENDCG}}FallBack "Diffuse"
}

视差映射 - 进阶版欺骗(“体感”欺骗)

视差映射的核心就是改变纹理坐标,用一张存储了模型信息的高度图,利用模型表面高度信息来对纹理进行偏移。

  • 解决的问题:法线贴图没有视差效果(即观察角度变化时,凹凸应该看起来有位移)。

  • 原理根据视角和高度,对UV坐标进行偏移

    1. 有一张高度图,存储了表面的高度信息(白色高,黑色低)。

    2. 在片段着色器中,根据视角方向 V 和该点的高度值 h,计算出UV应该偏移多少。
      UV_Offset = V.xy * (h * _Scale); // _Scale是一个控制强度的参数

    3. 用偏移后的新UV去采样法线贴图和颜色贴图。

  • 效果:比法线贴图更具体积感,尤其是斜着看的时候,凹凸感更真实。但偏移量过大时会出现采样错误(看起来像扭曲)。

  • 计算代价:中等。比法线贴图多一次高度图采样和简单的计算。

           // 视差映射函数float2 ParallaxMapping(float2 texCoords, float3 viewDir){ float height = tex2D(_DepthMap, texCoords).r;    float2 p = viewDir.xy / viewDir.z * (height * _HeightScale);return texCoords - p;    }

视差映射通常是在切线空间下进行的,因为高度图存储的高度信息是相对于切线空间的。因此,我们需要将世界空间的视线方向转换到切线空间,然后在切线空间进行视差偏移,最后再用偏移后的纹理坐标去采样法线贴图和漫反射贴图。

在顶点着色器中计算切线空间到世界空间的矩阵,以及世界空间到切线空间的逆矩阵。在片元着色器中,将世界空间的视线方向转换到切线空间。使用切线空间的视线方向进行视差映射,得到偏移后的纹理坐标。使用偏移后的纹理坐标采样法线贴图和漫反射贴图。

            v2f vert (appdata v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;// 转换法线、切线和副切线到世界空间o.worldNormal = normalize(mul(unity_ObjectToWorld, float4(v.normal, 0.0)).xyz);o.worldTangent = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);o.worldBinormal = normalize(cross(o.worldNormal, o.worldTangent) * v.tangent.w);// 计算世界空间视图方向float3 worldViewDir = normalize(_WorldSpaceCameraPos.xyz - o.worldPos);// 创建世界空间到切线空间的转换矩阵float3x3 worldToTangent = float3x3(o.worldTangent,o.worldBinormal,o.worldNormal);// 将视图方向转换到切线空间o.tangentViewDir = mul(worldToTangent, worldViewDir);TRANSFER_VERTEX_TO_FRAGMENT(o);return o;}fixed4 frag (v2f i) : SV_Target {// 应用视差映射float2 parallaxUV = ParallaxMapping(i.uv, i.tangentViewDir);// 从法线贴图获取切线空间法线float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, parallaxUV));// 创建切线空间到世界空间的转换矩阵float3x3 tangentToWorld = float3x3(i.worldTangent,i.worldBinormal,i.worldNormal);// 将法线从切线空间转换到世界空间float3 worldNormal = normalize(mul(tangentToWorld, tangentNormal));

陡峭视差映射 - 更逼真的欺骗

  • 解决的问题:基础视差贴图在陡峭角度和较大高度差时容易穿帮。想获得更精准的效果就需要的陡峭视差映射、其实陡峭视差映射也是一个近视的解,按时更精准,并对纹理坐标偏移进行合理检查。

  • 原理分层深度追踪

    1. 将高度范围(0到1)分成若干层(如10层)。

    2. 从最高层(1.0)或最低层(0.0)开始,沿着视角方向 V 一步步前进。

    3. 每一步都采样当前层的高度,并与当前点的深度进行比较。

    4. 当发现当前深度首次大于高度图采样的深度时(即穿过了表面),就找到了一个近似的交点。

    5. 用这个交点的UV坐标进行后续采样。

  • 效果:比普通视差贴图精确得多,效果更好,尤其适合深度变化较大的表面。

  • 计算代价:较高。需要多次(取决于层数)循环采样高度图,性能开销与迭代次数成正比。

// 陡峭视差映射函数
float2 SteepParallaxMapping(float2 texCoords, float3 viewDir)
{// 计算每层的深度步长float layerDepth = 1.0 / _NumLayers;// 当前层深度float currentLayerDepth = 0.0;// 每层的纹理坐标偏移量float2 P = viewDir.xy / viewDir.z * _HeightScale;float2 deltaTexCoords = P / _NumLayers;// 初始化纹理坐标和当前深度值float2 currentTexCoords = texCoords;float currentDepthMapValue = tex2Dlod(_DepthMap, float4(currentTexCoords, 0, 0)).r;// 使用for循环,最大迭代次数为_NumLayersfor (int i = 0; i < _NumLayers; i++){// 如果当前层深度已经大于深度图的值,则跳出循环if (currentLayerDepth >= currentDepthMapValue)break;// 偏移纹理坐标currentTexCoords -= deltaTexCoords;// 获取新纹理坐标处的深度值currentDepthMapValue = tex2Dlod(_DepthMap, float4(currentTexCoords, 0, 0)).r;// 增加深度currentLayerDepth += layerDepth;}// 获取碰撞前的纹理坐标(用于插值)float2 prevTexCoords = currentTexCoords + deltaTexCoords;// 获取碰撞前后的深度float afterDepth = currentDepthMapValue - currentLayerDepth;float beforeDepth = tex2Dlod(_DepthMap, float4(prevTexCoords, 0, 0)).r - (currentLayerDepth - layerDepth);// 线性插值计算权重float weight = afterDepth / (afterDepth - beforeDepth);// 插值得到最终纹理坐标float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);return finalTexCoords;
}

视差遮蔽映射 (POM) - 近乎真实的欺骗

解决的问题:陡峭视差映射虽然比基础视差映射精准,但其“分层步进”的本质决定了它仍然是一个离散化的近似解。在层数不足时,表面会呈现出“阶梯状”的层叠瑕疵。想要获得真正平滑、连续且精确的交点,并在此基础上实现自阴影效果,就需要视差遮蔽映射。

原理分层深度追踪 + 光线精确求交 + 自阴影

  1. 分层深度追踪(粗调和)

    • 和陡峭视差映射一样,首先将深度范围分成若干层,沿着视角方向 V 一步步前进,找到一个穿透表面的、包含精确交点的微小区间。这一步是快速的“粗调”。

  2. 光线精确求交(精调)

    • 在粗调找到的微小区间内(例如,第 n 层还在表面上方,第 n+1 层已经穿透表面),不再满足于用这一层或简单的线性插值作为最终结果。

    • 转而使用二分查找(Binary Search) 或其它光线追踪(Ray Marching) 技术,在这个极小的区间内进行多次迭代,精确地计算出视线射线与高度场表面的交点。这一步是精准的“精调”。

  3. 自阴影(Self-Shadowing)

    • 得到精确的交点 P 后,为了极致真实,可以再从点 P 向光源方向 L 发射一条新的射线。

    • 同样使用分层步进和精确求交的方法,检查这条新的射线是否会与自身的高度场再次相交。

    • 如果会相交,则说明点 P 处于自身投射的阴影中,需要相应地减弱其漫反射光照分量。

效果:是目前最顶级的视差效果模拟技术。

  • 极其精确的凹凸细节,消除了阶梯状瑕疵。

  • 动态的、准确的自阴影,这是产生立体感和真实感最重要的因素之一。

  • 在非极端角度下,视觉效果几乎可以媲美真正的几何镶嵌(Tessellation),但性能开销低得多。

计算代价非常高

  • 它需要 粗调步数 + 精调步数 次的高度图纹理采样。例如(10层粗调 + 5次二分查找 = 15次采样/像素)。

  • 自阴影功能需要再重复一次上述过程,相当于双倍的计算开销。

  • 性能开销与迭代次数成正比,是各种视差技术中最耗性能的一种,通常用于主机/PC平台的重要材质(如地面、墙壁),移动端需极度优化或避免使用。

            // 视差遮蔽映射函数 float2 ParallaxOcclusionMapping(float2 texCoords, float3 viewDir, out float parallaxHeight){// 计算每层的深度步长float layerDepth = 1.0 / _NumLayers;// 当前层深度float currentLayerDepth = 0.0;// 每层的纹理坐标偏移量float2 P = viewDir.xy / viewDir.z * _HeightScale;float2 deltaTexCoords = P / _NumLayers;// 初始化纹理坐标和当前深度值float2 currentTexCoords = texCoords;float currentDepthMapValue = 1.0 - tex2Dlod(_DepthMap, float4(currentTexCoords, 0, 0)).r;// 线性搜索 - 找到大致的相交区间for(int i = 0; i < _NumLayers; i++){// 如果当前层深度已经超过深度图值,提前退出循环if(currentLayerDepth >= currentDepthMapValue)break;// 偏移纹理坐标currentTexCoords -= deltaTexCoords;// 获取新纹理坐标处的深度值 (反转: 0=低, 1=高)currentDepthMapValue = 1.0 - tex2Dlod(_DepthMap, float4(currentTexCoords, 0, 0)).r;// 增加深度currentLayerDepth += layerDepth;}// 获取碰撞前的纹理坐标(用于二分查找)float2 prevTexCoords = currentTexCoords + deltaTexCoords;// 保存当前深度和前一深度用于二分查找float depthAfter = currentDepthMapValue;float depthBefore = 1.0 - tex2Dlod(_DepthMap, float4(prevTexCoords, 0, 0)).r;// 二分查找 - 精确交点定位float2 intersectTexCoords = currentTexCoords;for(int j = 0; j < _BinarySearchSteps; j++){// 计算中间点float2 midTexCoords = (prevTexCoords + currentTexCoords) * 0.5;float midDepth = 1.0 - tex2Dlod(_DepthMap, float4(midTexCoords, 0, 0)).r;// 调整搜索区间if(currentLayerDepth < midDepth){prevTexCoords = midTexCoords;depthBefore = midDepth;}else{currentTexCoords = midTexCoords;depthAfter = midDepth;}}// 计算最终权重float weight = (currentLayerDepth - depthAfter) / (depthBefore - depthAfter + 0.0001);float2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);// 返回视差高度用于自阴影计算parallaxHeight = currentLayerDepth;return finalTexCoords;}

// 自阴影计算函数
float CalculateSelfShadow(float2 texCoords, float3 lightDir, float parallaxHeight)
{// 计算光线方向的偏移float2 lightOffset = lightDir.xy / lightDir.z * _HeightScale * parallaxHeight;// 采样阴影处的深度float shadowDepth = 1.0 - tex2D(_DepthMap, texCoords + lightOffset).r;// 计算阴影强度float shadow = shadowDepth < parallaxHeight ? _ShadowStrength : 1.0;return shadow;
}

优化

 动态层次调整

视差遮蔽映射采用的是光线步进,当高度层次划分的越多,那么得到的结果越精确,但是随之而来的就是计算量的增大,为了优化计算量,我们可以动态的调节高度分层,避免了在所有情况下都使用最大采样次数,减少不必要的计算。

根据视角与表面法线的夹角动态调整采样层数

当视角垂直于表面时(dot(viewDir, normal) ≈ 1),视差效果不明显,使用较少层数

当视角接近平行于表面时(dot(viewDir, normal) ≈ 0),视差效果显著,需要更多层数

float numLayers = lerp(_MaxLayers, _MinLayers, abs(dot(float3(0, 0, 1), viewDirTS)));
Jitter(抖动采样)

RayMarching采样次数过少会导致分层伪影的问题,有很明显的分层感。一般会采用Jitter(抖动)解决。通过引入随机偏移,每个像素的采样起点随机偏移,打破采样点的规律性排列。即使使用较少的采样层数,也能获得更平滑、更自然的结果。

// 抖动的随机噪声
float RandomNoise(float2 uv)
{   uv += 1 * float2(47.0, 17.0) * 0.695;const float3 magic = float3(0.06711056, 0.00583715, 52.9829189);return frac(magic.z * frac(dot(uv, magic.xy)));
}...// 在循环前添加抖动
float layerDepth = 1.0 / numLayers;
float currentLayerDepth = layerDepth * RandomNoise(i.uv); // 添加抖动起点

位移映射 - 不再是欺骗

  • 原理真正地修改顶点的几何位置

    • 顶点着色器曲面细分着色器中,采样高度图。

    • 直接沿着法线方向位移顶点 Vertex.xyz += Normal * (height * _Scale)

  • 效果完美。它真实地改变了模型的几何形状,因此拥有正确的轮廓、遮挡和阴影。是效果最好的技术。

  • 计算代价:最高。

    • 如果用在顶点着色器,需要模型本身有足够多的顶点来表现细节。

    • 最佳实践:与曲面细分着色器配合使用。先通过曲面细分生成大量的新顶点,再对这些顶点进行位移。这是目前3A游戏表现超高细节的主要手段。


文章转载自:

http://BaxIBO47.pqcrz.cn
http://d1LWr1Ar.pqcrz.cn
http://8ResnBMJ.pqcrz.cn
http://us2pnJiK.pqcrz.cn
http://ZMhfV59c.pqcrz.cn
http://gOW2qhty.pqcrz.cn
http://Y777HHdy.pqcrz.cn
http://ifV6beB2.pqcrz.cn
http://CoiNpk0Y.pqcrz.cn
http://We9B8BA4.pqcrz.cn
http://epjZDuI8.pqcrz.cn
http://8qoIc4VB.pqcrz.cn
http://W2w1iVDl.pqcrz.cn
http://tIanyCyj.pqcrz.cn
http://23hb8exm.pqcrz.cn
http://EE07BbeU.pqcrz.cn
http://IJJ6hHOP.pqcrz.cn
http://WMUYs4La.pqcrz.cn
http://59SXJOB6.pqcrz.cn
http://KgnYSsEO.pqcrz.cn
http://vAzPDo0I.pqcrz.cn
http://IqkaJorM.pqcrz.cn
http://SFcb40ng.pqcrz.cn
http://t2LE22XG.pqcrz.cn
http://LKzMfivS.pqcrz.cn
http://IYrxfIfa.pqcrz.cn
http://DssAGtfK.pqcrz.cn
http://kU00so8D.pqcrz.cn
http://vfjsDB3Z.pqcrz.cn
http://qlrLybgZ.pqcrz.cn
http://www.dtcms.com/a/385409.html

相关文章:

  • 嵌入式硬件——I.MX6U-Mini 蜂鸣器(BEEP)模块
  • LeetCode 2799.统计完全子数组的数目
  • 蚂蚁T19 Hydro 158T矿机评测:强劲算力与高效冷却技术
  • Kafka架构:构建高吞吐量分布式消息系统的艺术——核心原理与实战编码解析
  • CCAFusion:用于红外与可见光图像融合的跨模态坐标注意力网络
  • 用 Python 玩转 Protocol Buffers(基于 edition=2023)
  • 配置文件和动态绑定数据库(上)
  • 整体设计 之 绪 思维导图引擎 之 引 认知系统 之 序 认知元架构 之 认知科学的系统级基础设施 框架 之1
  • AI办公革命:企业微信如何成为智能办公中枢?
  • 企业微信AI功能实操指南:智能表格与邮件如何提升协作效率?
  • 04 完成审批任务
  • keil出现 cmsis_compiler.h(279): error: #35: #error directive: Unknown compilr解决方法
  • CSS `:has()` 实战指南:让 CSS 拥有“if 逻辑”
  • 【开题答辩全过程】以 Java校园二手书城平台为例,包含答辩的问题和答案
  • 机器视觉在新能源汽车电池中有哪些检测应用
  • CES Asia的“五年计划”:打造与北美展比肩的科技影响力
  • 王梦迪团队推出TraceRL:迈向扩散语言模型「RL大一统」
  • 运用脚本部署lamp架构
  • Springboot项目中引入ES(一)
  • 专项智能练习(认知主义学习理论)
  • Mysql索引总结(1)
  • Spring Boot中的Binder类基本使用和工具封装
  • 数字化工厂建设:是简单组装PLM/ERP/MES/WMS等系统,还是彻底重构?
  • 带你了解STM32:OLED调试器
  • 软考中项考几门?多少分合格?
  • 1.5 调用链分层架构 - mybatis源码学习
  • 线性代数 · 矩阵 | 秩 / 行秩 / 列秩 / 计算方法
  • 期权时间价值会增长么?
  • 数据结构(陈越,何钦铭) 第十讲 排序(下)
  • Java——JVM