Unity反射式红点瞄准镜Shader原理分析

项目上要实现 Unity 真实瞄准镜的功能,其中之一就是反射式红点瞄准镜。本文分析的 Shader 以模拟反射式瞄准镜光学原理的方式实现了较为真实的效果。
效果图
该图展示了从不同角度观看红点瞄准镜的效果。
红点瞄准镜原理
反射式红点瞄准镜的特殊之处在于不管眼睛和照门等是否三点一线,只要看到红点套在目标上,就代表已瞄准目标。
首先上一张灵魂插图:
上图表示了反射式红点瞄准镜最简单的组成部分,即光源+凹面镜,原理如下:
- 凹面镜为透明镜子,前方景物的反射光可以穿透。同时凹面镜有特殊镀膜,使得光源的光可以反射回去。
-
光源位于凹面镜的焦点,根据物理原理,其光线经过凹面镜反射后为平行光。
-
人眼接收到某些平行光时,可以脑补出无穷远处有一个红点。
-
由于光源的光线反射后为平行光,因此从侧面看,红点会发生偏移,当角度过大时,红点就完全看不到了。
反射式红点瞄准镜 Shader
代码
该 Shader 核心代码只有七行,其中最关键代码为前四行:
void surf (Input IN, inout SurfaceOutput o) {
float shortestDistanceToSurface = dot (_WorldSpaceCameraPos - IN.worldPos,IN.worldNormal);
float3 closestPoint = _WorldSpaceCameraPos - (shortestDistanceToSurface * IN.worldNormal);
float2 uv_Delta = (mul((float3x3)unity_WorldToObject,IN.worldPos) - mul((float3x3)unity_WorldToObject,closestPoint)).xy * _uvScale;
half4 col = tex2D(_reticleTex,(0.5f, 0.5f) + uv_Delta/shortestDistanceToSurface);
o.Emission = (col.a * _reticleColour.rgb * _reticleBright);
o.Albedo = max(col.a * _reticleColour.rgb, _glassTrans * _glassColour.rgb);
o.Alpha = max(col.a, _glassTrans);
}
解析
- 找到离平面最近距离及交点
首先找到摄像机到镜片表面的最近距离,用点乘计算,得shortestDistanceToSurface
。计算摄像机到镜片平面最短路径的接触点,得closestPoint
(不考虑 z 轴)。 - 计算红点贴图采样偏移
根据上文瞄准镜原理可知,在物理世界中,反射的红点光线均为平行光,因此若最短路径接触点在镜片区域中,则该点一定是红点图案的中心。
在本地坐标下,计算closestPoint
与本顶点位置的偏移值,该值只取 x 和 y,该数值的意义是:当前顶点相对于红点纹理中心的偏移量。
将偏移量乘上一个用于调节红点图案缩放的可调变量_uvScale
,之后使用 tex2D 采样,其中 (0.5f,0.5f) 为红点纹理的中心,加上偏移量,即为该顶点的纹理采样。
这里使用uv_Delta/shortestDistanceToSurface
的目的是使得图案在正常范围内尽量不随距离改变而改变。也就是说,希望不管距离镜片多远,虚拟红点大小都一致,没有近大远小的透视效果。
当然该除法并不严谨,因为物体所占的视场角大小不仅和距离摄像机距离有关,还和物体与摄像机中心的偏移量、摄像机视场角有关。但是该除法在实际使用场景用已经足够了,在可视范围内图案大小变化不是特别明显。 - 设置顶点参数
之后,将采样得到的颜色按需做些特殊处理,最后设置顶点的颜色等参数。
关于 VR
若使用 ShaderLab 中的表面着色器,一般情况下 Unity 会自动处理左右眼的渲染,即_WorldSpaceCameraPos
的值会根据当前渲染的眼睛发生变化。因此,我们无需手动计算左右眼的情况。
若使用普通的顶点着色器或者片元着色器,则可能需要判断当前渲染的眼睛来设置特定参数值。Unity 提供了渲染眼睛状态判断以及相应的瞳距等参数,对应取值判断并计算即可。