【游戏开发实战】教你在Unity中实现笼中窥梦的效果(RenderTexture | 视觉差| 多相机 | 渲染 | shader | 多场景)

一、前言

嗨,大家好,我是新发。
最近去面大厂了,一直在充电,然后,前天,有同学私信我,问我笼中窥梦的效果用Unity如何实现,
在这里插入图片描述
如下是笼中窥梦的游戏画面
请添加图片描述
不错,然后,我就用Unity实现了一下比较基础的效果,如下,
请添加图片描述
请添加图片描述

请添加图片描述
请添加图片描述
下面我就来讲一下我的制作思路和过程吧~

二、实现思路

我先讲一下实现思路。
需求一:
在一个盒子的每个面上可以看到多个场景的画面,并且具有透视效果
思路:
在盒子的每个面上通过RenderTexture来显示画面,每张RenderTexture对应一个小场景的摄像机的渲染画面,然后RenderTexture的材质球的shader需要实现3D透视效果

需求二:
盒子在某个视觉角度下,多个场景之间的物体会在视觉上联系起来,触发机关
思路:
触发条件可以理解为主摄像机的一个角度和距离,党摄像机与这个条件很接近时就触发多场景机关动画

三、具体实现过程

1、场景建模

我用Blender简单制作了场景模型,主要是盒子的框架和面,
请添加图片描述

2、一些小物件模型

我之前在AssetStore上买过一个Low Poly模型包,
在这里插入图片描述
里面有很多小零件模型和低模人形模型,
请添加图片描述
喜欢Low Poly风格的同学可自行去AssetStore上下载:https://assetstore.unity.com/packages/3d/props/exterior/polygon-prototype-low-poly-3d-art-by-synty-137126

3、制作RenderTexture

Project视图中右键菜单,点击Create / Render Texture即可创建RenderTexture
在这里插入图片描述
盒子的前后左右,再加上顶部,总共5个面,需要5RenderTexture,如下
在这里插入图片描述

4、制作小场景

事实上,我是在一个Unity场景中制作了多个小场景,每个小场景都带一个独立摄像机进行渲染,他们相互之间在空间距离上错开,如下
在这里插入图片描述
在这里插入图片描述
小场景一:
在这里插入图片描述
小场景二:
在这里插入图片描述
小场景三:
在这里插入图片描述
小场景四:
在这里插入图片描述
小场景五:
在这里插入图片描述

5、小场景摄像机渲染到RenderTexture

把刚刚我们创建的5RenderTexture分别赋值给小场景中的摄像机的TargetTexture,如下,
在这里插入图片描述
这样子,摄像机就不会直接渲染到屏幕上了,而会渲染到我们设置的这张RenderTexture上,如下,
请添加图片描述

6、制作材质球

盒子的面是网格(Mesh),网格要渲染需要材质球(Material),我们分别创建5个材质球,如下,
在这里插入图片描述
把材质球分别赋值给盒子的面,如下
在这里插入图片描述

7、shader实现透视立体效果

如果上面的材质球使用普通的Unlit/Texture作为shader,效果是这个鬼样,从侧面看的时候,它失去了立体效果,你可以想象你从侧面看电视机的那个样子,而实际上,我们需要的是从侧面看窗外的那种效果,
请添加图片描述
这里我的思路是先在顶点着色器阶段计算齐次裁剪坐标系下的屏幕坐标,缓存起来,然后到片元着色器阶段的时候先去齐次(即除以w分量),然后换算成uv,再对纹理进行采样,这样子得出来的结果就等价于小场景的画面是平铺在整个屏幕上的,然后经过了盒子网格的裁切,就有了那种透过窗户看世界的效果了,我写的shader代码如下,我写了注释,比较简单,大家如果有shader基础的话应该能看懂,

Shader "linxinfa/BoxWorld"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
        // 屏幕高与宽的比值,默认720/1280,即0.5625
        _ScreenHW ("ScreenHW", Float) = 0.5625
        
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
                float4 screenPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float _ScreenHW;


            v2f vert (appdata v)
            {
    
    
                v2f o;
                // 把顶点坐标从局部坐标转化到齐次裁剪空间
                o.vertex = UnityObjectToClipPos(v.vertex);
                // 计算屏幕坐标,注意这时的坐标是齐次空间下的屏幕坐标
                o.screenPos = ComputeScreenPos(o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                // 去齐次
                float2 screenPos = i.screenPos.xy / i.screenPos.w;
                // 根据屏幕坐标来算uv
                float2 uv = screenPos.xy * float2(1, _ScreenHW);
                // 采样
                float4 col = tex2D(_MainTex, uv);
                return col;
            }
            ENDCG
        }
    }
}

把材质球的shader设置为linxinfa/BoxWorld,如下
在这里插入图片描述
现在就是透过窗户看世界的效果了,
请添加图片描述

8、触发机关动画

机关条件就是主摄像机的某个坐标和角度,这个我们可以做一个配置表,简单的机关可以做成一个动画,触发了机关就播放动画即可,当然也有复杂一点的机关,可以根据具体需求去实现。
我做的机关动画状态机如下,
在这里插入图片描述
其中level1动画如下,
请添加图片描述

9、GamePlay脚本

最后就是写C#脚本来驱动游戏了,我只写了一个脚本GamePlay.cs
脚本的逻辑就是根据鼠标控制摄像头的移动和旋转,判断主摄像头的坐标和角度是否达到触发机关的条件,然后触发机关,播放机关动画,逻辑不复杂,可以看我写的注释,代码如下,

using System;
using System.Collections;
using UnityEngine;

public class GamePlay : MonoBehaviour
{
    
    
    // 小场景摄像机
    [SerializeField] private Transform[] littleSceneCams;
    // 小场景中心(相机围绕次中心旋转)
    [SerializeField] private Transform[] littleSceneCenters;

    // 盒子
    [SerializeField] private Transform box;

    // 主摄像机
    [SerializeField] private Transform camMain;


    private float m_deltaX;
    private float m_deltaY;
    [SerializeField] private float m_rotateSpeed;

    [SerializeField] private Vector3 lookOffset = new Vector3(0, 1, 0);

    [SerializeField] private Vector3 lookFaceOffset = new Vector3(0, -0.5f, 0);

    [SerializeField] private float moveYSpeed = 0.05f;

    [SerializeField] private float moveZSpeed = 0.1f;

    private int level = 0;

    [SerializeField] private Animator ani;

    private bool canRotate = true;

    void Update()
    {
    
    
        if (!canRotate || !Input.GetMouseButton(0)) return;
        m_deltaX = Input.GetAxis("Mouse X");
        m_deltaY = Input.GetAxis("Mouse Y");

        if (m_deltaX == 0 && m_deltaY == 0)
        {
    
    
            return;
        }
        // 左右旋转盒子
        box.Rotate(Vector3.up, -m_deltaX);
        // 左右旋转每个小场景
        for (int i = 0, len = littleSceneCenters.Length; i < len; ++i)
        {
    
    
            littleSceneCenters[i].Rotate(Vector3.up, -m_deltaX * m_rotateSpeed);
        }


        // 移动主摄像机
        camMain.localPosition += new Vector3(0, m_deltaY * moveYSpeed, m_deltaY * moveZSpeed);


        // 限制主摄像机的移动区域
        if (LimitCamPos()) return;

        // 移动小场景摄像机
        for (int i = 0, len = littleSceneCams.Length; i < len; ++i)
        {
    
    
            var cam = littleSceneCams[i];
            cam.localPosition += new Vector3(0, m_deltaY * moveYSpeed, m_deltaY * moveZSpeed);
            cam.LookAt(littleSceneCenters[i].position + lookFaceOffset);
        }

        // 检查是否触发了机关
        CheckLevelCondition();
    }

    // 限制主摄像机的移动区域 
    private bool LimitCamPos()
    {
    
    
        var curPos = camMain.position;
        var isOut = false;
        if (curPos.z < -5f)
        {
    
    
            curPos.z = -5f; isOut = true;
        }

        if (curPos.z > 1.2f)
        {
    
    
            curPos.z = 1.2f; isOut = true;
        }
        if (curPos.y > 4.8f)
        {
    
    
            curPos.y = 4.8f; isOut = true;
        }
        if (curPos.y < 1.3f)
        {
    
    
            curPos.y = 1.3f; isOut = true;
        }
        camMain.position = curPos;
        camMain.LookAt(box.position + lookOffset);
        return isOut;
    }

    /// <summary>
    /// 检测是否触发了机关
    /// </summary>
    private void CheckLevelCondition()
    {
    
    
       if (Vector3.Distance(camMain.position, new Vector3(0, 2.242501f, -3.405001f)) <= 0.5f &&
            Math.Abs(camMain.localEulerAngles.x - 20.047f) <= 2 &&
            0 == level)
        {
    
    
            Debug.Log("触发了机关");
            StartCoroutine(NextLevel());
        }
    }


    // 下一关
    private IEnumerator NextLevel()
    {
    
    
        canRotate = false;
        ++level;
        ani.SetInteger("level", level);
        yield return null;
        ani.SetInteger("level", 0);
        // 四秒后才允许旋转
        yield return new WaitForSeconds(4);
        canRotate = true;
    }
}

在场景中创建一个空物体,重命名为GamePlay,并挂上GamePlay脚本,设置成员变量,如下,
在这里插入图片描述

10、运行测试

最终运行Unity,测试效果如下,

请添加图片描述
请添加图片描述
请添加图片描述

四、工程源码

本文Demo工程我已上传到CODE CHINA,感兴趣的同学可自行下载学习,
地址:https://codechina.csdn.net/linxinfa/UnityVisionDiffBox
注意:我使用的Unity版本为2021.1.7f1c1,如果你使用的Unity版本与我不同,可能打开工程时会有兼容问题
在这里插入图片描述

五、完毕

好啦,就到这里吧~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~

猜你喜欢

转载自blog.csdn.net/linxinfa/article/details/121683783