[UnityShader] Triplanar 纹理映射教程笔记

作者:谷某某
2020-09-29
12 9 0

Triplanar 笔记

这个笔记主要记录自 CatlikeCoding-Triplanar 上面的文章很长而且有很多跟 Triplanar 无关的内容,其实 triplanar 和核心内容就是很简单的几行代码,所以看我这个笔记就可以了。

小学生都能看懂的视频版,求三连

什么是 Triplanar?为什么要用 Triplanar?

一般来说我们想要映射纹理到我们的模型上,可以用根据每个顶点的 uv 坐标来映射纹理。但这不是唯一的方法,而 triplanar 就是另一种映射贴图的方式。有时候,uv 坐标映射的方式可能无法达到我们想要的效果(比如造成贴图拉伸)甚至根本没有 uv 坐标,这些情况大多在用代码生成模型时,动态改变模型时发生。Triplanar 的原理就是用顶点位置作为 uv 坐标,用法线作为权重,来实现贴图的映射,因为映射已 xyz 三个平面分别映射,所以叫做 tri-planar。

《A Short Hike》中的 triplanar 应用

先放源码

Shader "Custom/Triplanar" {
Properties {
    _TopTex("TopTexture", 2D) = "white" {}
    _SideTex("SideTexture", 2D) = "white" {}
    _BlendOffset("BlendOffset",Range(0,0.5)) = 0.25
    _BlendExponent ("Blend Exponent", Range(1, 8)) = 2
}
SubShader {
    Tags { "RenderType"="Opaque" }
Pass {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityStandardBRDF.cginc"
      #include "Assets/Plugins/GumouKit/cgincs/gfunc.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float3 normal:NORMAL;
      };
      struct v2f
      {
        float4 vertex : SV_POSITION;
        float3 normal:TEXCOORD1;
        float4 worldpos : TEXCOORD2;
      };

      fixed _BlendOffset;
      half _BlendExponent;
      sampler2D _TopTex;
      sampler2D _SideTex;
      float4 _TopTex_ST,_SideTex_ST;

      struct TriUV{
        float2 xUV,yUV,zUV;
      };
      TriUV GetTriUV (float4 worldpos) {
        TriUV triUV;
        triUV.xUV = worldpos.zy;
        triUV.yUV = worldpos.xz;
        triUV.zUV = worldpos.xy;
        return triUV;
      }
      fixed3 GetTriWeights(fixed3 normal){
        fixed3 weights = abs(normal);   //因为 normal 可能有负数
        weights = saturate(weights-_BlendOffset);   //控制权重
        weights = pow(weights,_BlendExponent);  //进一步控制权重
        return weights/(weights.x+ weights.y+ weights.z);   //使 xyz 相加=1
      }

      v2f vert (appdata v)
      {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.normal = UnityObjectToWorldNormal(v.normal);
        o.worldpos = mul(unity_ObjectToWorld,v.vertex);
        return o;
      }
      fixed4 frag (v2f i) : SV_Target
      {
        //triplanar
        TriUV triuv = GetTriUV(i.worldpos);
        fixed4 colx = tex2D(_SideTex,triuv.xUV * _SideTex_ST.xy + _SideTex_ST.zw);
        fixed4 coly = tex2D(_TopTex,triuv.yUV * _TopTex_ST.xy + _TopTex_ST.zw);
        fixed4 colz = tex2D(_SideTex,triuv.zUV * _SideTex_ST.xy + _SideTex_ST.zw);
        fixed3 weights = GetTriWeights(i.normal);
        fixed4 tricol = colx*weights.x + coly*weights.y +colz*weights.z;
        return tricol;
      }
      ENDCG
    }
  }
}

用顶点位置作为 uv 坐标

先来映射一个面

fixed4 frag (v2f i) : SV_Target
{
  fixed4 colx = tex2D(_SideTex,i.worldpos.zy);
  fixed4 coly = tex2D(_TopTex,i.worldpos.xz);
  fixed4 colz = tex2D(_SideTex,i.worldpos.xy);
  //return colx;
  //return colx;
  return colx;
}

可以写成一个函数方便使用

struct TriUV{
  float2 xUV,yUV,zUV;
};
TriUV GetTriUV (float4 worldpos) {
  TriUV triUV;
  triUV.xUV = worldpos.zy;
  triUV.yUV = worldpos.xz;
  triUV.zUV = worldpos.xy;
  return triUV;
}
fixed4 frag (v2f i) : SV_Target
{
  TriUV triuv = GetTriUV(i.worldpos);
  fixed4 colx = tex2D(_SideTex,triuv.xUV);
  fixed4 coly = tex2D(_TopTex,triuv.yUV);
  fixed4 colz = tex2D(_SideTex,triuv.zUV);
}

合在一起的效果

fixed4 frag (v2f i) : SV_Target
{
  TriUV triuv = GetTriUV(i.worldpos);
  fixed4 colx = tex2D(_SideTex,triuv.xUV);
  fixed4 coly = tex2D(_TopTex,triuv.yUV);
  fixed4 colz = tex2D(_SideTex,triuv.zUV);
  return (colx+coly+colz)/3;
}

用法线作为权重

fixed3 GetTriWeights(fixed3 normal){
  fixed3 weights = abs(normal);   //因为 normal 可能有负数
  return weights/(weights.x+ weights.y+ weights.z);   //使 xyz 相加=1
}
......
fixed4 frag (v2f i) : SV_Target
{
  .....
  fixed3 weights = GetTriWeights(i.normal);
  fixed4 tricol = colx*weights.x + coly*weights.y +colz*weights.z;
  return tricol;
}

控制权重

Properties {
  .....
  _BlendOffset("BlendOffset",Range(0,0.5)) = 0.25
}
.....
fixed _BlendOffset;
....
fixed3 GetTriWeights(fixed3 normal){
  fixed3 weights = abs(normal);   //因为 normal 可能有负数
  weights = saturate(weights-_BlendOffset);   //控制权重
  return weights/(weights.x+ weights.y+ weights.z);   //使 xyz 相加=1
}
....
fixed4 frag (v2f i) : SV_Target
{
.....
fixed3 weights = GetTriWeights(i.normal);
  fixed4 tricol = colx*weights.x + coly*weights.y +colz*weights.z;
  return tricol;
}

进一步控制权重

Properties {
  .....
  _BlendOffset("BlendOffset",Range(0,0.5)) = 0.25
  _BlendExponent ("Blend Exponent", Range(1, 8)) = 2
}
.....
fixed _BlendOffset;
half _BlendExponent;
....
fixed3 GetTriWeights(fixed3 normal){
  fixed3 weights = abs(normal);   //因为 normal 可能有负数
  weights = saturate(weights-_BlendOffset);   //控制权重
  weights = pow(weights,_BlendExponent);  //进一步控制权重
  return weights/(weights.x+ weights.y+ weights.z);   //使 xyz 相加=1
}
....
fixed4 frag (v2f i) : SV_Target
{
.....
fixed3 weights = GetTriWeights(i.normal);
  fixed4 tricol = colx*weights.x + coly*weights.y +colz*weights.z;
  return tricol;
}

接下来我们可以试试映射多张贴图,我们 y 轴用 _TopTex 草地贴图,xz 轴用 _MainTex 石头贴图。

Properties {
  .....
  _MainTex ("Texture", 2D) = "white" {}
  _TopTex("",2D) = "white"{}
}
...
fixed4 frag (v2f i) : SV_Target
{
  fixed4 colx = tex2D(_MainTex,i.worldpos.zy);
  fixed4 coly = tex2D(_TopTex,i.worldpos.xz);
  fixed4 colz = tex2D(_MainTex,i.worldpos.xy);
}


最后我们再实现一下纹理的缩放和位移

float4 _MainTex_ST,_SideTex_ST;
.....
fixed4 frag (v2f i) : SV_Target
{
  fixed4 colx = tex2D(_MainTex,i.worldpos.zy * _MainTex_ST.xy + _MainTex_ST.zw);
  fixed4 coly = tex2D(_TopTex,i.worldpos.xz * _SideTex_ST.xy + _SideTex_ST.zw);
  fixed4 colz = tex2D(_MainTex,i.worldpos.xy * _MainTex_ST.xy + _MainTex_ST.zw);
  ...
}

把缩放设置为 5 看一下效果


结束。