嗨,大家好,我是新发。
最近去面大厂了,一直在充电,然后,前天,有同学私信我,问我笼中窥梦的效果用Unity
如何实现,
如下是笼中窥梦的游戏画面
不错,然后,我就用Unity
实现了一下比较基础的效果,如下,
我先讲一下实现思路。
需求一:
在一个盒子的每个面上可以看到多个场景的画面,并且具有透视效果
思路:
在盒子的每个面上通过RenderTexture
来显示画面,每张RenderTexture
对应一个小场景的摄像机的渲染画面,然后RenderTexture
的材质球的shader
需要实现3D
透视效果
需求二:
盒子在某个视觉角度下,多个场景之间的物体会在视觉上联系起来,触发机关
思路:
触发条件可以理解为主摄像机的一个角度和距离,党摄像机与这个条件很接近时就触发多场景机关动画
我用Blender
简单制作了场景模型,主要是盒子的框架和面,
我之前在AssetStore
上买过一个Low Poly
模型包,
里面有很多小零件模型和低模人形模型,
喜欢Low Poly
风格的同学可自行去AssetStore
上下载:https://assetstore.unity.com/packages/3d/props/exterior/polygon-prototype-low-poly-3d-art-by-synty-137126
在Project
视图中右键菜单,点击Create / Render Texture
即可创建RenderTexture
,
盒子的前后左右,再加上顶部,总共5
个面,需要5
张RenderTexture
,如下
事实上,我是在一个Unity
场景中制作了多个小场景,每个小场景都带一个独立摄像机进行渲染,他们相互之间在空间距离上错开,如下
小场景一:
小场景二:
小场景三:
小场景四:
小场景五:
把刚刚我们创建的5
张RenderTexture
分别赋值给小场景中的摄像机的TargetTexture
,如下,
这样子,摄像机就不会直接渲染到屏幕上了,而会渲染到我们设置的这张RenderTexture
上,如下,
盒子的面是网格(Mesh
),网格要渲染需要材质球(Material
),我们分别创建5
个材质球,如下,
把材质球分别赋值给盒子的面,如下
如果上面的材质球使用普通的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
,如下
现在就是透过窗户看世界的效果了,
机关条件就是主摄像机的某个坐标和角度,这个我们可以做一个配置表,简单的机关可以做成一个动画,触发了机关就播放动画即可,当然也有复杂一点的机关,可以根据具体需求去实现。
我做的机关动画状态机如下,
其中level1
动画如下,
最后就是写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;
}
///
/// 检测是否触发了机关
///
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
脚本,设置成员变量,如下,
最终运行Unity
,测试效果如下,
本文Demo
工程我已上传到CODE CHINA
,感兴趣的同学可自行下载学习,
地址:https://codechina.csdn.net/linxinfa/UnityVisionDiffBox
注意:我使用的Unity
版本为2021.1.7f1c1
,如果你使用的Unity
版本与我不同,可能打开工程时会有兼容问题
好啦,就到这里吧~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~