视差映射在 Unity Shader 的实现

铁名_IronName Lv4

在Unity中,基于 Phong 着色 Blinn-Phong 光照模型,使用法线贴图和高度贴图,实现了基础视差,陡峭视差,视差闭塞,浮雕映射 的 Shader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
Shader "Test/Better Bump Mapping"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
[Space(20)]
[Header(Parallax Mapping)]
[KeywordEnum(No, Parallax, Steep, Occlusion, Relief)] _ParallaxModel ("Parallax Model", Float) = 0
_HeightMap ("Height Map", 2D) = "white" {}
_Scale("Scale(For Parallax)", Range(-0.05, 0.05)) = 0.01
_MaxSteps("Step(For Steep/Relief)", Range(1,20)) = 8
}
SubShader
{
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
#pragma multi_compile _PARALLAXMODEL_NO _PARALLAXMODEL_PARALLAX _PARALLAXMODEL_STEEP _PARALLAXMODEL_OCCLUSION _PARALLAXMODEL_RELIEF

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
// float4 _BumpMap_ST;
float _BumpScale;
sampler2D _HeightMap;
// float4 _HeightMap_ST;
fixed4 _Specular;
float _Gloss;
float _Scale;
int _MaxSteps;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);

o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);

float2 uv = i.uv;

// --- 视差偏移计算 ---
#if defined(_PARALLAXMODEL_PARALLAX)
float height = 1 - tex2D(_HeightMap, uv).r;
float2 offset = height * tangentViewDir.xy * _Scale / max(tangentViewDir.z, 0.01);
uv += offset;
#elif defined(_PARALLAXMODEL_STEEP)
// 陡峭视差(固定层数)
float stepSize = 1.0 / _MaxSteps;
float dx = ddx(i.uv);
float dy = ddy(i.uv);

float2 deltaUV = tangentViewDir.xy * _Scale / (tangentViewDir.z * _MaxSteps); // 每步偏移量
float curHeight = 1.0-tex2Dgrad(_HeightMap, uv, dx, dy).r;
float rayHeight = 0.0; // 从顶部开始
for (int j = 0; j < _MaxSteps; j++) {
rayHeight = stepSize * (j + 1.0);
uv += deltaUV; // 沿视线向里移动
curHeight = 1.0-tex2Dgrad(_HeightMap, uv, dx, dy).r;
if (rayHeight > curHeight) break; // 穿过表面
}

#elif defined(_PARALLAXMODEL_OCCLUSION)
float stepSize = 1.0 / _MaxSteps;
float dx = ddx(i.uv);
float dy = ddy(i.uv);

float2 deltaUV = tangentViewDir.xy * _Scale / (tangentViewDir.z * _MaxSteps); // 每步偏移量
float2 preUV = uv;
float curHeight = 1.0-tex2Dgrad(_HeightMap, uv, dx, dy).r;
float rayHeight = 0.0; // 从顶部开始
for (int j = 0; j < _MaxSteps; j++) {
preUV = uv;
rayHeight = stepSize * (j + 1.0);
uv += deltaUV; // 沿视线向里移动
curHeight = 1.0-tex2Dgrad(_HeightMap, uv, dx, dy).r;
if (rayHeight > curHeight) break; // 穿过表面
}

float preHeight = 1.0 - tex2Dgrad(_HeightMap, preUV, dx, dy).r;
float prevDiff = preHeight - rayHeight + stepSize; // 射线在前一层高出表面的距离
float curDiff = rayHeight - curHeight; // 射线在当前层低于表面的距离
float weight = prevDiff / (prevDiff + curDiff); // [0,1] 之间的比例

uv = lerp(preUV, uv, weight);
#elif defined(_PARALLAXMODEL_RELIEF)
// 陡峭视差步进:找到交点所在区间
float stepSize = 1.0 / _MaxSteps;
float dx = ddx(i.uv);
float dy = ddy(i.uv);

float2 deltaUV = tangentViewDir.xy * _Scale / (tangentViewDir.z * _MaxSteps); // 每步偏移量
float2 uv0 = uv; // 前一层 UV
float2 uv1 = uv; // 当前层 UV(初始化为相同,步进后更新)
float rayDepth = 0.0; // 当前射线深度(从表面顶部开始)
float prevSurfaceDepth = 1.0 - tex2Dgrad(_HeightMap, uv, dx, dy).r; // 前一层表面深度
float curSurfaceDepth = prevSurfaceDepth;

// 步进搜索,找到射线首次穿过表面的位置
for (int j = 0; j < _MaxSteps; j++) {
rayDepth += stepSize; // 射线进入下一层
uv1 += deltaUV; // UV 移动到下一层
curSurfaceDepth = 1.0 - tex2Dgrad(_HeightMap, uv1, dx, dy).r; // 当前层表面深度

if (rayDepth > curSurfaceDepth) {
break;
}
uv0 = uv1;
prevSurfaceDepth = curSurfaceDepth;
}

// 此时:
// uv0 对应射线深度 rayDepth - stepSize(前一层)
// uv1 对应射线深度 rayDepth(当前层)
// prevSurfaceDepth 是前一层表面深度(应 ≤ 前一层射线深度)
// curSurfaceDepth 是当前层表面深度(应 < 当前射线深度)

// 二分搜索(8次迭代足够)
float t0 = rayDepth - stepSize; // 前一层射线深度
float t1 = rayDepth; // 当前层射线深度
float2 uvMid;
float midDepth;

[unroll] //不生成跳转指令,直接把循环体复制 8 次。
for (int k = 0; k < 8; k++) {
float tMid = (t0 + t1) * 0.5;
// UV 在两点间线性插值(因为 UV 与深度成线性关系)
uvMid = lerp(uv0, uv1, (tMid - t0) / (t1 - t0));
midDepth = 1.0 - tex2Dgrad(_HeightMap, uvMid, dx, dy).r; // 采样中点表面深度

if (midDepth > tMid) {
// 表面深度大于射线深度,交点在上半区
t0 = tMid;
uv0 = uvMid;
} else {
// 表面深度小于等于射线深度,交点在下半区
t1 = tMid;
uv1 = uvMid;
}
}

// 最终使用中点 UV 作为交点
uv = lerp(uv0, uv1, 0.5); // 或直接使用 uvMid(最后一次循环已更新)
#else
// _PARALLAXMODEL_NO:什么都不做
#endif

// 从法线纹理中获取纹素
fixed4 packedNormal = tex2D(_BumpMap, uv);
fixed3 tangentNormal;
// 设置纹理为“法线映射”,使用内置函数
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

fixed3 albedo = tex2D(_MainTex, uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(tangentNormal, tangentLightDir));

fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, halfDir)), _Gloss);
fixed3 color = ambient + diffuse + specular;
// color = fixed3(height, height, height);

return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}

  • 标题: 视差映射在 Unity Shader 的实现
  • 作者: 铁名_IronName
  • 创建于 : 2026-02-23 17:31:43
  • 更新于 : 2026-02-23 17:37:27
  • 链接: https://blog.ironname.top/2026/02/23/视差映射在-Unity-Shader-的实现/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
视差映射在 Unity Shader 的实现