如何在Unity中完美实现视差滚动Parallax效果

2,745 阅读3分钟

模型分析

视差滚动的本质就是不同深度(即transform.position.z,以下简写zz)的图片移动速率不同。那么不同层级的移动速率有什么样的特点呢?

想象一下,摄像机拍摄玩家在正中心位置,当玩家往右移动nn个单位距离时,摄像机也移动相同的距离。考虑3种极限情况:

  1. 和玩家的zz相同

    这一层的图片不需要移动,在摄像机画面中,它自然而然地会相对往左移动nn个单位距离,因此我们可以把这一层的Parallax FactorParallax\ Factor设为00

  2. 处于摄像机的远裁切面

    我们把处于远裁切面定义为无穷远处,很明显,处于无穷远处的物体在摄像机看来似乎是定格在画面中的,因此该层的图片需要相同的往右移动nn个单位距离以保持相对静止,这一层的Parallax FactorParallax\ Factor设为11

  3. 处于摄像机的近裁切面

    与远裁切面正好相反,这一层的图片需要与角色移动方向相反,我们人为地定义这一层的Parallax FactorParallax\ Factor1-1

因此,我们可以得出结论,在摄像机近、远裁切面之间不同zzParallax FactorParallax\ Factor区间为[1,1][-1, 1],与玩家所在的zz系数是00Parallax FactorParallax\ Factorzz正相关。所以,我们只需要把背景层的zz值映射到这个区间即可。

A4 12.png

Parallax FactorParallax\ Factor计算公式

Camera.zCamera.z:摄像机的transform.position.z

Player.zPlayer.z:玩家的transform.position.z

zz: 当前物体的transform.position.z

Camera.z+nearCamera.z + near:计算得到的摄像机近裁切面的zz值,nearnear是摄像机的nearClipPlane

Camera.z+farCamera.z + far:计算得到的摄像机远裁切面的zz值,farfar是摄像机的farClipPlane

  • 当处于前景层时

    Parallax Factor=zPlayer.zPlayer.z(Camera.z+near)Parallax\ Factor = \frac{z-Player.z}{Player.z-(Camera.z + near)}

  • 当处于背景层时

    Parallax Factor=zPlayer.zCamera.z+farPlayer.zParallax\ Factor = \frac{z-Player.z}{Camera.z + far-Player.z}

代码形式

using UnityEngine;

public class Parallax : MonoBehaviour

{

    [SerializeField] private Transform player;

    private Vector3 startPosition;
    private Vector3 cameraStartPosition;

    private float cameraStartX => cameraStartPosition.x;
    private float cameraTravelX => Camera.main.transform.position.x - cameraStartX;
    private float distanceFromPlayer => transform.position.z - player.position.z;
    private float playerToClippingPlaneLength => distanceFromPlayer > 0
        ? Camera.main.transform.position.z + Camera.main.farClipPlane - player.position.z
        : player.position.z - Camera.main.transform.position.z - Camera.main.nearClipPlane;
    private float parallaxFactor => distanceFromPlayer / playerToClippingPlaneLength;

    private void Awake()
    {
        startPosition = transform.position;
        cameraStartPosition = Camera.main.transform.position;
    }

    private void Update()
    {
        float newX = startPosition.x + cameraTravelX * parallaxFactor;
        transform.position = new Vector3(newX, startPosition.y, startPosition.z);

    }
}

在Unity编辑器中实操

  1. 设置相机为正交相机,设置近、远裁切面

截屏2024-06-08 00.52.59.png

  1. 导入背景图,为每一层背景设置不同的transform.position.z,需要确保能被摄像机渲染出来。图层的Draw Mode设置为Tiled,把Width调整为较大的值(这个要根据你实际的场景和需求设置,若不够长,当玩家走到边缘时会穿帮)。

截屏2024-06-07 23.14.03.png

  1. 为了方便测试,这里简单地把主摄像机设为Player的子物体以快速实现跟随效果,同时添加了玩家移动脚本以展示效果。

  2. 运行后,效果如图:

Jun-07-2024 23-30-26.gif

优化

我们注意到,我们需要手动设置Tiled模式下宽度的值,导致场景中有一长串图片很影响开发模式下的观感。有没有方法使图片保持简洁且效果不变呢?只需通过Shader修改纹理采样的偏移即可!

这里通过Shader Graph实现,暴露OffsetX变量在代码中修改即可。

截屏2024-06-07 23.57.32.png

调整步骤

  1. 使用创建的Shader Graph生成数个Material,将Texture修改为背景图
  2. Hierarchy窗口中创建Plane,修改Material为上步创建的材质球;修改Plane的Rotation为(90,0,180)(90, 0, 180)使得能在画面中正确显示;Scale属性中的x设置为你的背景图片的width/heightwidth / height,Scale属性中的z设置为11
  3. 将这些背景层的对象移动成为Player的子物体
  4. 修改Parallax脚本为如下,并添加到创建的Plane上(修改部分做了注释说明,此处不再赘述)
using UnityEngine;

public class Parallax : MonoBehaviour
{
    [SerializeField] private Transform player;

    private MeshRenderer meshRenderer;
    private Vector3 cameraStartPosition;

    // 计算得到图片的宽度
    private float imageWidth => Camera.main.orthographicSize * 2 * transform.lossyScale.x;
    private float cameraStartX => cameraStartPosition.x;
    private float cameraTravelX => Camera.main.transform.position.x - cameraStartX;
    private float distanceFromPlayer => transform.position.z - player.position.z;
    private float playerToClippingPlaneLength => distanceFromPlayer > 0
        ? Camera.main.transform.position.z + Camera.main.farClipPlane - player.position.z
        : player.position.z - Camera.main.transform.position.z - Camera.main.nearClipPlane;
    private float parallaxFactor => distanceFromPlayer / playerToClippingPlaneLength;

    private void Awake()
    {
        meshRenderer = GetComponent<MeshRenderer>();
        cameraStartPosition = Camera.main.transform.position;
    }

    private void Update()
    {
        // 偏移量是移动距离与图片宽度的比值
        // 注意这里乘的系数是1 - parallaxFactor
        meshRenderer.material.SetFloat("_OffsetX", (1 - parallaxFactor) * cameraTravelX / imageWidth);
    }
}

我们看一下运行效果

Jun-08-2024 01-28-54.gif

最后,这里只实现了xx方向的视差滚动,如果需要或只要yy方向(比如飞机大战)视差滚动,那么只需修改少量代码、shader稍作修改即可,这里不再演示。