news 2026/10/17 21:58:00

Triplanar三平面映射:破解程序化地形UV拉伸的Shader方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Triplanar三平面映射:破解程序化地形UV拉伸的Shader方案

1. 三平面映射到底在解决什么痛点

1.1 一个让人头疼的真实场景

前阵子我在做某游戏项目的地形系统,需要给一大片悬崖峭壁贴材质。普通模型上,美术会预先拆好UV,把一张岩石贴图映射到网格表面上。但悬崖这种地形网格,往往是用程序化算法生成的,顶点分布不规则,坑坑洼洼的凹面和凸面交错在一起,UV拆出来的效果惨不忍睹——纹理拉伸、接缝明显、重复感强,正面看着还行,侧面就直接糊成了马赛克。

当时团队里有人提议把UV修一修,但程序化生成的地形每次跑出来网格都不一样,手动修UV根本不现实。后来我去翻了翻一些大厂的技术分享,发现很多开放世界游戏的地形和岩石渲染都在用一种叫Triplanar Mapping(三平面映射)的方案。查了一圈资料,又在自己的项目里实测了几个版本,彻底把这块吃透了。这篇文章把我的完整思路、踩过的坑以及可以“抄作业”的代码都整理出来,希望能帮到正在被UV问题折磨的读者。

1.2 普通UV映射为什么会失效

在讲三平面映射之前,先理清普通UV映射的原理。通常美术在建模软件里为网格生成一套二维坐标,叫UV坐标,代表网格表面上的每个点在纹理贴图上的对应位置。渲染时,GPU从每个像素的UV坐标去纹理图里取颜色,再把颜色贴到三角形上。

问题在于,UV坐标是一张“平面展开图”,而三维物体表面是凹凸不平、朝向各异的。平面展开图描述不了复杂曲面上发生的形变。想象把一张世界地图直接贴在乒乓球上,表面褶皱的地方地图必然变形。我们项目里那些悬崖,顶部区域和垂直面之间的过渡带,形变最严重,纹理被拉成了细长条,看起来就像老电影里转场的镜头扭曲效果。

更麻烦的是接缝。地形网格在程序化生成的时候,是由大量Patch拼接而成的,相邻Patch之间的拓扑和顶点分布可能完全不同。如果每个Patch各自展开UV,接缝处就很难做到纹理连续。哪怕美术再细心地手动调整,几百上千个Patch挨个调,工期也耗不起。

1.3 三平面映射的思路:放弃UV

三平面映射的思路非常直接:不要UV了,直接用世界坐标作为采样依据。具体来说,它把三维空间中的一个点,分别向三个坐标轴方向做投影,得到三套二维坐标,分别去采样纹理,再根据表面法线的朝向,把三次采样结果按权重混合起来。

举个例子,一个石壁上的点,世界坐标是(x, y, z)。我们从X轴方向看,把它投影到YZ平面上,得到一个坐标(y, z),用这个坐标去采样岩石纹理;从Y轴方向看,投影到XZ平面,得到(x, z),再采样一次;从Z轴方向看,投影到XY平面,得到(x, y),再采样一次。三个采样结果都拿到后,看这个点的法线朝哪个方向——朝上多一点,那就多取Y方向投影的结果;朝前往右多一些,就多取X或Z方向的结果。最终混合在一起,得到一个连续、没有明显拉伸的纹理效果。

这个思路之所以能解决悬崖那种复杂网格的UV问题,是因为它完全不依赖网格自身的拓扑和UV展开。只要世界坐标是连续的,三个方向的投影就是连续的,采样结果自然连续。不管网格多碎、多扭曲,只要顶点在空间中的位置没变,采样的效果就不会乱。对于程序化生成的地形、洞穴、桥梁等场景,这个特性简直是福音。

2. 从零搭起一个可用的Triplanar Shader

2.1 Shader整体结构与属性声明

理论说清楚了,接下来直接进入实战。我先写一个最基础的Triplanar Shader,保证在Unity里能跑、效果直观可见,后续再在这个基础上加高级功能。Shader用的是Unity的ShaderLab语法,渲染管线以内置管线(Built-in Render Pipeline)为例,对URP的适配我后面会单独说。

Shader "Custom/TriplanarBasic" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _Tiling ("Tiling", Float) = 1.0 _Sharpness ("Sharpness", Range(1, 16)) = 4.0 _Color ("Color Tint", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert vertex:vert #pragma target 3.0 sampler2D _MainTex; float _Tiling; float _Sharpness; fixed4 _Color; struct Input { float3 worldPos; float3 worldNormal; }; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input,o); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); } void surf (Input IN, inout SurfaceOutput o) { // 计算三个方向投影的UV坐标 float2 uvX = IN.worldPos.zy * _Tiling; // X轴方向投影,取ZY平面 float2 uvY = IN.worldPos.xz * _Tiling; // Y轴方向投影,取XZ平面 float2 uvZ = IN.worldPos.xy * _Tiling; // Z轴方向投影,取XY平面 // 采样三次 fixed3 colX = tex2D(_MainTex, uvX).rgb; fixed3 colY = tex2D(_MainTex, uvY).rgb; fixed3 colZ = tex2D(_MainTex, uvZ).rgb; // 根据世界法线计算混合权重 float3 weights = pow(abs(IN.worldNormal), _Sharpness); weights /= (weights.x + weights.y + weights.z); // 最终颜色 = 三次采样结果按权重加权混合 fixed3 col = colX * weights.x + colY * weights.y + colZ * weights.z; col *= _Color.rgb; o.Albedo = col; o.Alpha = 1.0; } ENDCG } FallBack "Diffuse" }

这段代码是整篇文章的地基,我逐个部分拆开讲。属性栏里四个参数:

  • _MainTex:基础纹理,贴墙、贴地用的主纹理
  • _Tiling:纹理在世界空间里的重复尺寸。数值越大,单位面积内的纹理重复次数越多,相当于“纹理缩放”
  • _Sharpness:混合权重锐度,控制三个方向过渡带的宽窄
  • _Color:整体颜色Tint,用来调节明暗

Input结构体里声明了worldPos和worldNormal。我在顶点着色器vert里手动计算了它们——unity_ObjectToWorld把模型局部顶点坐标转到世界坐标,UnityObjectToWorldNormal把模型法线转到世界空间。这两步很关键,稍不注意法线方向算错了,混合权重就会完全错乱。

2.2 投影坐标的计算逻辑

片元着色器里最核心的就是三个投影UV的计算:

  • X轴方向投影:取世界坐标的zy分量
  • Y轴方向投影:取世界坐标的xz分量
  • Z轴方向投影:取世界坐标的xy分量

为什么X方向投影取的是zy而不是yz?从X轴正方向看过去,能看到的平面是由Y和Z轴张成的,所以X方向投影对应的坐标是( y, z )。这个顺序我一开始也搞反过,当时出来的纹理方向全错了,后来对着世界方向反复确认才调过来。

_Tiling直接影响纹理密度。比如_Tiling设为1,世界空间中每1个单位长度对应纹理的一个重复周期;设为4,每0.25个单位长度就重复一次。这个参数在实际项目里要按纹理内容微调,比如岩石纹理细节多,可以设大一点;大面积的草地,设小一号显得比较自然。

2.3 权重混合公式的推导与验证

混合权重这步是整段着色器的灵魂。先取世界法线的绝对值——注意是绝对值,因为法线朝向负方向时,它仍然指向那个投影平面,我们关注的是角度差而不是方向。然后做幂运算:

float3 weights = pow(abs(IN.worldNormal), _Sharpness); weights /= (weights.x + weights.y + weights.z);

为什么要做幂运算?直接用法线分量做权重行不行?比如法线是(0.7, 0.7, 0.0),直接归一化后权重各占大约一半,在45度斜面上,X和Y两个方向的采样结果各自贡献一半,混合区会显得很宽,纹理边缘发虚。用幂次把权重“压”向主方向,法线分量大的方向权重被放大,分量小的方向被压得更小。_Sharpness越大,过渡区越窄,三个方向的边界越锐利;_Sharpness越小,过渡越柔和。

这一步还需要归一化,保证三个分量加起来等于1,混合结果不会整体偏亮或偏暗。

验证一下:一个完全朝上的平面,世界法线是(0, 1, 0),abs后是(0, 1, 0),pow之后还是(0, 1, 0),归一化后权重是(0, 1, 0)——完全使用Y方向投影的结果,即从顶上看下来的纹理。这符合直觉,因为朝上的平面就应该用从顶向下的投影。朝左的平面,法线(1, 0, 0),权重就是(1, 0, 0),完全用X方向投影。只有在斜面上,才会有多个方向的投影混合。

注意:这里的worldNormal必须归一化,否则abs和pow做完以后,权重归一化之前的结果会包含长度信息,最终颜色可能偏暗偏亮。我用UnityObjectToWorldNormal已经把法线归一化了一次,但有些第三方代码里法线可能没有归一化,出问题先检查这一行。

3. 混合细节与参数调优的实战经验

3.1 Sharpness到底调到多少合适

我在项目里反复调过_Sharpness,总结出来的经验是:常规地形和岩石,4到8之间效果最好。小于2的时候,过渡带太宽,峡谷、悬崖边缘会有明显的“雾蒙蒙”感,纹理像蒙了一层半透明的纱,细节全丢。大于16以后,过渡带窄到几乎只有几个像素,效果反而像直接把表面切成了三个硬边界,斜坡拐弯处会出现比较生硬的色彩断层。

但这也不是绝对值,要看你的网格尺度和纹理内容。我在测试场景里放了一个半径5米的圆球和一个100米高的悬崖做对比,球体表面曲率变化快,_Sharpness调8到10才舒服;悬崖大平面过渡缓和,4就够。实操建议是从4开始,渲染出来看地面转45度角的地方有没有明显糊边,再小幅增减。

3.2 Tiling的坑:纹理密度变化引发的问题

_Tiling越大,纹理越小越密,远处看会有一种类似摩尔纹的闪烁感,尤其在相机移动的时候非常明显。这在高清纹理上尤其严重,因为纹理本身细节多,频率高,投影到远处像素的时候采样率不够,就会产生混叠。

项目里遇到过这样的场景:某段悬崖的陡坡,从上往下看纹理很密,侧面看又感觉变稀疏了。原因是陡坡在世界空间里按单位长度重复纹理,但当坡面在屏幕上投影面积变小的时候,视觉上的纹理密度没变,可实际像素覆盖的纹理跨度在变大,观感就失衡了。

常用的缓解方案有几种。一种是在顶点着色器里根据片元距相机的距离动态调整_Tiling——但片元着色器已经拿到世界坐标了,在surf函数里做也行,只是要额外传一个相机参数。另一种更干脆:给纹理加一层很细微的噪点混合,打断摩尔纹的规律性,但要小心别把整体亮度带偏。我后来更推荐用高质量的纹理过滤方式,配合生成mipmap,能解决大部分闪烁问题。

3.3 用三张纹理混合,还是用一张纹理的多个通道

基础的Triplanar Shader只用了一张纹理。实际项目里,岩石和地面往往需要更丰富的混合效果,比如岩石上覆盖一层薄薄的苔藓,或者泥土和沙地过渡。很多项目会准备多张纹理,分别作为基底、细节层、湿滑层,然后通过世界高度、坡度、噪声图来实时控制混合比例。

我做过一个版本,用三张纹理分别放在RGB通道里:R通道存苔藓,G通道存岩石肌理,B通道存泥土。然后在片元着色器里对每张纹理做Triplanar采样,再根据高度和法线方向混合。效果比单纹理直接丰富很多,但代价是三倍多的采样次数,性能开销直接上去。移动端跑这个Shader,帧率明显下滑,后来我改用一张纹理集(Texture Array)把三张纹理打包,采样次数没变,但通过动态索引减少了带宽压力。

3.4 法线贴图遇上Triplanar:一个容易翻车的组合

基础Albedo的Triplanar很好写,一旦要加法线贴图,坑就来了。法线贴图的方向是相对于局部坐标系的,Triplanar采样时纹理方向被扭曲了,如果还按原始的法线贴图方式直接作用,物体表面光照会很怪异——明明是凹陷的地方凸起来,凸起的地方反而凹陷。

我踩过这个坑之后,查了业界通用的处理方式:对三方向采样,分别构建对应的切线空间变换矩阵,把每个方向采样的法线变换回世界空间,再合并。这是一个比较大的话题,简化的做法是:

  • 分别在X、Y、Z方向采样法线贴图,得到三个切线空间的法线值
  • 用对应的投影轴方向构建一个3x3旋转矩阵,把切线空间的法线转换到世界空间
  • 三个世界空间法线按Albedo的同一套权重混合

代码量大约会增加一倍,但效果非常明显,岩石表面的立体感完全不一样了。这个版本我放在后面“进阶玩法”小节里详细展开。

4. 进阶玩法:从地表到洞穴的完整方案

4.1 支持法线贴图的完整Triplanar Shader

上文提到法线贴图需要单独处理,这里贴出完整版本。这个版本我实测在Unity 2021和2022的内置管线下都能正常渲染,关键点在注释里标注清楚。

Shader "Custom/TriplanarNormal" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _BumpMap ("Normal Map", 2D) = "bump" {} _Tiling ("Tiling", Float) = 1.0 _Sharpness ("Sharpness", Range(1, 16)) = 8.0 _Color ("Color Tint", Color) = (1, 1, 1, 1) _NormalStrength ("Normal Strength", Range(0, 2)) = 1.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert vertex:vert #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; float _Tiling; float _Sharpness; fixed4 _Color; float _NormalStrength; struct Input { float3 worldPos; float3 worldNormal; INTERNAL_DATA }; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input,o); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); } // 将切线空间法线通过指定的投影轴变换到世界空间 float3 WorldNormalFromTangent (float3 tangentNormal, float3 axis) { // 构建正交化基向量 float3 T = normalize(cross(axis, float3(0, 1, 0))); // 如果axis接近Y轴,cross会有问题,需要换一个参考向量 if (abs(axis.y) > 0.999) T = normalize(cross(axis, float3(1, 0, 0))); float3 B = cross(axis, T); // tangentNormal是xy分量在切线空间里的法线 float3 worldNormal = tangentNormal.x * T + tangentNormal.y * B + tangentNormal.z * axis; return normalize(worldNormal); } void surf (Input IN, inout SurfaceOutput o) { float2 uvX = IN.worldPos.zy * _Tiling; float2 uvY = IN.worldPos.xz * _Tiling; float2 uvZ = IN.worldPos.xy * _Tiling; // Albedo三次采样 fixed3 colX = tex2D(_MainTex, uvX).rgb; fixed3 colY = tex2D(_MainTex, uvY).rgb; fixed3 colZ = tex2D(_MainTex, uvZ).rgb; // 法线贴图三次采样 fixed3 nX = UnpackNormal(tex2D(_BumpMap, uvX)); fixed3 nY = UnpackNormal(tex2D(_BumpMap, uvY)); fixed3 nZ = UnpackNormal(tex2D(_BumpMap, uvZ)); // 权重计算 float3 weights = pow(abs(IN.worldNormal), _Sharpness); weights /= (weights.x + weights.y + weights.z); // 权重平滑(可选:避免某些角度导致个别权重为0) float3 blend = smoothstep(0.2, 0.8, weights); // 混合Albedo fixed3 albedo = colX * blend.x + colY * blend.y + colZ * blend.z; albedo *= _Color.rgb; // 混合法线 float3 nrmWorld = float3(0,0,0); nrmWorld += WorldNormalFromTangent(nX, float3(1, 0, 0)) * blend.x; nrmWorld += WorldNormalFromTangent(nY, float3(0, 1, 0)) * blend.y; nrmWorld += WorldNormalFromTangent(nZ, float3(0, 0, 1)) * blend.z; nrmWorld = normalize(nrmWorld); nrmWorld = lerp(IN.worldNormal, nrmWorld, _NormalStrength); o.Albedo = albedo; o.Normal = nrmWorld; o.Alpha = 1.0; } ENDCG } FallBack "Diffuse" }

这段代码里最需要注意的是WorldNormalFromTangent函数:输入的是从法线贴图采样得到的切线空间法线,以及当前投影轴方向,输出的是世界空间法线。它的原理是:切线空间的Z轴就是表面法线方向,在Triplanar场景下,我们可以近似地把投影轴方向当作表面法线方向来构建基向量。

另外,如果法线本身是从X方向投影采样的,基向量的构建就要以X轴为“表面法线”来推导,否则方向会错。《渲染中的数学》里有完整推导,这里不展开,但记住一点:三个投影轴分别对应世界空间的X、Y、Z,两个切向量T和B必须与轴正交。

4.2 高度混合:让地形过渡更自然

只有法线贴图还不够,实际场景里,石壁和地面交界处往往有泥土、落叶覆盖,单纯用权重混合,边界会很干净,反而显得“假”。这时可以在Triplanar基础上加入高度混合。

做法是在片元着色器里多采样一张高度图,或者用噪声生成一个高度值,然后根据当前点的世界Y坐标和噪声值,决定Albedo和法线的混合权重。我记得当时为了模拟山脚碎石和山上雪线的自然过渡,生成了一张大范围的噪声图,映射到世界XZ平面作为高度控制,在高度超过阈值的地方把岩石纹理调亮变白,低于阈值的地方混入泥土纹理,过渡区域用smoothstep做平滑。配合Triplanar的权重混合,效果非常自然,完全看不到纹理接缝。

高度混合还有另一个妙用:解决简单的Triplanar在垂直大平面上的“重复感”。当Tiling很大时,一整面墙全是同一个纹理的重复排列,肉眼很容易看出规律。加了一层随空间变化的高度噪声后,纹理的明暗在小范围内起伏,视觉上破坏了重复规律。

4.3 程序化生成场景时的封装思路

程序化生成地形或洞穴时,我们通常希望材质参数能在运行时动态控制,而不是每个Patch都新建一个Material实例。做法是提前创建少数的几个Material实例,然后公有属性让C#脚本统一设置。比如把_Tiling和_Sharpness暴露成地形系统配置文件里的字段,生成地形Patch时直接给材质赋值,大幅减少运行时材质数量和Draw Call。

另一个经验是:Triplanar通常配合**Texture Array(纹理数组)**使用。程序化生成的地表往往包含多种材质,比如岩石、泥土、草地、苔藓。每创建一个材质都做Triplanar会成倍增加采样开销。用Texture Array把同一组纹理的多个变体打包,在Shader里根据顶点颜色或高度信息动态选择纹理索引,一个Pass就能完成多种材质的混合,性价比很高。

5. 移动端和URP环境下的适配与优化

5.1 采样次数膨胀:核心性能瓶颈

Triplanar最直接的性能代价是纹理采样次数成倍增加。普通Lambert只需要采一次Albedo,Triplanar至少三次,加上法线贴图就直接六次。移动端GPU的采样带宽有限,六次采样对中低端机压力很大,容易造成帧率骤降和发热。

我的优化思路首先是控制分辨率。移动端上把法线贴图分辨率做成Albedo的一半,或者用ETC2/ASTC压缩格式,都能显著降低带宽占用。其次是提前在C#端检查相机距离,远距离的Patch只开启Albedo的Triplanar,近处才开启法线贴图,用LOD思路分层渲染。实测在骁龙中端芯片上,近景6次采样、远景3次采样,整体帧率比全程6次采样高15%左右,效果肉眼几乎没差。

5.2 URP下的迁移要点

如果项目用的是URP(Universal Render Pipeline),上面基于Surface Shader的写法不能直接用,需要改成HLSL Shader,并把光照函数换成URP的Standard PBR函数。核心Triplanar算法部分不变,差异主要在结构和语义。

URP的Shader Pass要自己声明Tags"LightMode" = "UniversalForward",顶点着色器的输出结构需要包含positionCS和worldPos、worldNormal。片元着色器里用UniversalFragmentPBR函数来计算光照和颜色。混合权重和采样的逻辑完全照搬CG版的代码模块,改成HLSL语法即可。

实践上更快的迁移方式是用Shader Graph:在Shader Graph里添加Custom Function节点,把Triplanar的采样和权重计算封装成HLSL文件,用SubGraph的方式接入。这样既能保留可视化调参的便利,又能复用现成的代码逻辑。我后来把项目里所有地形材质都改成了Shader Graph + Custom Function的方案,维护起来比纯手写HLSL省心很多。

5.3 移动端的特殊注意事项

移动端的GPU架构跟桌面端差异大,有几个细节值得单独说。一个是半精度浮点的问题,世界坐标的数值范围可能很大,移动端如果用half精度存储worldPos,距离原点太远时精度不足会导致纹理抖动。我在Shader里给worldPos开了float精度,其他计算保持half,平衡了精度和性能。

还有一点是关于纹理过滤的。Triplanar的投影采样在斜面上会跨越多个纹理像素,如果纹理mipmap层级变化太突兀,远处和近处的纹理清晰度跳变会很明显。我开了anisotropic filtering配合trilinear filtering,并且设置_MainTex的MipMap Bias为-0.5,从实际效果看,洞穴项目的远景稳定性和近景锐度都有改善。这部分经验可能跟具体的GPU驱动有关,但方向是通用的。

6. 常见问题速查与独家避坑经验

6.1 纹理方向错乱

这是最容易遇到、也最好排查的问题:纹理上的条纹方向跟模型方向对不上,比如横纹变成了竖纹。原因基本只有一个——投影平面取错了。我在前面强调过,X轴投影要取世界坐标的zy,因为从X轴方向看过去,可见平面的两个基向量就是Z和Y。如果你写反了,比如取成yz或者用yz乘了不同的系数,方向就会错乱。排查时先在片元着色器里把三个采样结果分别用纯色标记输出,看一眼哪块区域颜色不对,就知道哪个投影轴取反了。

6.2 斜面上出现明显的纹理拉伸

有时候权重计算没问题,但斜面的纹理还是拉伸得厉害。这通常发生在_Sharpness设置偏小的时候,斜面上X和Y方向的采样结果各占一半,两个方向投影的纹理拉伸方向不一样,混在一起的视觉效果就是“糊”、锐度低。把_Sharpness调大,或者改成用世界法线和三个轴的夹角做反向权重(角度越接近90度,权重越小),能明显缓解。另一种情况是世界法线在Vertex Shader阶段没有被正确归一化,导致pow的结果被缩放,这时要检查UnityObjectToWorldNormal的输出,并在片元里手动normalize一下。

6.3 阴影接收和光照异常

Surface Shader模式下,如果法线混合出了错,表现往往不是纹理糊,而是光照怪:亮的亮、暗的暗,跟周围地形不协调。最常见的原因是我在WorldNormalFromTangent里构建基向量时的那个分支判断,当投影轴接近Y轴时,如果还用float3(0,1,0)做cross,结果会是零向量。我写了一个if (abs(axis.y) > 0.999)的分支,换用float3(1,0,0)来避免这种情况。这个细节容易忽略,但一旦触发就是整块区域法线全错。建议在实验阶段把这个分支保留着,不要随便删。

6.4 移动端纹理闪烁

上一篇已经提了一部分,这里补充一个排查思路:如果闪烁只在相机运动时出现,大概率是mipmap选择问题;如果静态时也闪,就要检查是不是世界坐标精度不够。我用float精度后,静态闪烁基本消失;相机运动时的闪烁靠调MipMap Bias和开启各向异性过滤解决。另外,Tiling如果大于64,高频纹理的闪烁会非常明显,这时候有必要考虑在片元着色器里做一层简单的frac降频采样,或者用细节纹理叠加放大。

6.5 接缝处上下两半颜色不一致

在程序化生成的洞穴里,顶部和底部分别用不同材质甚至不同Shader渲染,如果边界正好落在Triplanar的过渡带上,会因为相邻Patch的_Sharpness不同导致颜色跳变。我的处理方式是把_Sharpness、_Tiling这类参数写入Patch的统一配置,确保相邻Patch用同一组参数,从根源上避免不一致。如果没法统一,可以在边缘做一层基于世界坐标的渐变混合,用smoothstep把两种材质在交界处融合掉。

7. 与具体项目结合的个人体会

做完这个地形系统的Triplanar改造之后,我对它的理解从“一个纹理采样技巧”变成了“一种渲染架构思路”。它最大的价值不是修UV,而是解耦了纹理映射与网格拓扑。只要物体表面在世界空间里有定义,就能得到可预测的纹理效果。这一点对程序化生成、作品展示类的Demo、以及美术资产复用场景价值极大。

但也不是说Triplanar是银弹。平面感很强的网格,比如室内地板、桌面,用普通UV映射更高效;只有表面朝向复杂、UV展开成本高的地方才值得上Triplanar。判断标准很朴素:你给这张纹理的UV调试花的时间,如果超过了写Triplanar Shader的时间,那就果断换方案。

最后分享一个我在项目里用的小技巧:Triplanar的投影方向不一定要锁死在世界空间。在某些固定在世界坐标里的场景(比如洞穴、城池),世界空间是对的;但如果是飘浮平台、可以旋转的空中岛屿,改用物体空间或自定义锚点空间会更好。使用物体空间时,Shader里把unity_ObjectToWorld换成unity_ObjectToWorld逆矩阵的处理方向即可,代码改动不大,但适配场景能扩大很多。根据我自己的经验,提前想清楚投影空间坐标系,能省掉不少返工时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 21:57:40

Qt串口控制程控电源:RS232电平、握手与时序实战指南

简介:这是一份基于Qt框架开发的RS232串口电源控制程序,面向嵌入式系统初学者、电子工程实践者及自动化设备控制开发者,解决实验室/测试场景中对程控电源的远程开关与状态交互需求。项目采用C与Qt构建跨平台GUI应用,通过串口指令实…

作者头像 李华
网站建设 2026/10/17 21:56:43

opencode 安装 minimaxskill 报 401?把 endpoint 改到 TaoToken 的排查记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 21:52:22

PLC智能照明控制:工业级可靠性解决灯控失控问题

简介:本资源是一份面向自动化、电气工程及智能控制方向初学者与课程设计学生的PLC应用实践文档,聚焦解决传统照明开关(声控、触摸、感光型)在复杂场景下响应失准、无法自启自停、节能效果有限等实际问题。文档以郑州电力职业技术学…

作者头像 李华
网站建设 2026/10/17 21:50:21

esp32开发环境搭建:VS Code+ESP-IDF 从零配置到编译烧录一次跑通

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华