本来以为全景图很简单,把全景图纹理设置为“Cube”类型,弄个天空盒材质附上去,然后摄像机弄个角度旋转脚本,就完事了。。。但是,用天空盒渲染全景图的话,没有办法缩放。。
于是,还是得用球体啊。。用Blender建一个球,法线反转一下,就是让球里面的纹理是正面,然后摄像机摆到球中心,球的材质,shader选Unlit/Texture即可。
第一直觉,以为把球体放大和缩小,就能实现缩放,呵呵。。然而并不可以,只要球体x、y、z三轴是等比例缩放,无论缩放系数如何,看到的效果是完全一样的。
所以,正确缩放的方法是,改变摄像机的位置。。但是摄像机旋转时,要绕中心点旋转,相当于,摄像机摆在一个较小的同心球的球面上,转动或者缩放该同心球,就能得到对应的缩放效果。也就是说,缩放就是改变摄像机运行轨道的半径,如下图所示:
using System;
using UnityEngine;
using UnityEngine.EventSystems;
public class Camera360Controller : MonoBehaviour
{
// 缩放系数
public static float ZoomRate = 0.5f;
// 最远距离(摄像机轨道最大半径)
public static float MaxDistance = 2f;
// 旋转系数
public static float RotationRate = 0.5f;
// 最小俯仰角度限制
public static float BottomClampAngle = -80;
// 最大俯仰角度限制
public static float TopClampAngle = 80;
private Vector2 lastMouse;
private float pitch;
private float distance;
private float lerp = 1;
// 调用该函数,在0.5秒内重置摄像机初始角度和位置(回到中心点)
public void ResetCamera()
{
lerp = 0;
}
private void FixedUpdate()
{
// 在0.5秒内摄像机回到中心点
if (lerp < 1f)
{
lerp = Mathf.Clamp(lerp + 2 * Time.fixedDeltaTime, 0f, 1f);
var trans = transform;
trans.position = Vector3.Lerp(trans.position, Vector3.zero, lerp);
trans.rotation = Quaternion.Lerp(trans.rotation, Quaternion.identity, lerp);
distance = Mathf.Lerp(distance, 0, lerp);
pitch = Mathf.Lerp(distance, 0, lerp);
}
}
private void Update()
{
// 摄像机在重置过程中,不接受用户其他操作
if (lerp < 1f)
return;
// 兼顾触控屏和鼠标
switch (Input.touchCount)
{
case 1:
{
// 单指触控,旋转摄像机视角,手指在UI上时放弃
var touch = Input.GetTouch(0);
if (EventSystem.current.IsPointerOverGameObject(touch.fingerId))
return;
if (touch.phase == TouchPhase.Moved)
{
CameraRotation(touch.deltaPosition);
}
break;
}
case 2:
{
// 双指触控,移动相机,缩放全景图
var touch1 = Input.GetTouch(0);
var touch2 = Input.GetTouch(1);
// 如果手指在UI上,放弃
if (EventSystem.current.IsPointerOverGameObject(touch1.fingerId) ||
EventSystem.current.IsPointerOverGameObject(touch2.fingerId))
return;
var oldpos1 = touch1.position - touch1.deltaPosition;
var oldpos2 = touch2.position - touch2.deltaPosition;
float olddis = (oldpos1 - oldpos2).magnitude;
float nowdis = (touch1.position - touch2.position).magnitude;
// 计算新的摄像机轨道半径
distance = Mathf.Clamp(distance + (nowdis - olddis) * ZoomRate, -MaxDistance, MaxDistance);
var trans = transform;
trans.position = trans.forward * distance;
break;
}
default:
{
// 检测鼠标操作,点在UI上,放弃
if (EventSystem.current.IsPointerOverGameObject())
return;
if (Input.GetMouseButtonDown(1))
{
lastMouse = Input.mousePosition;
}
else if (Input.GetMouseButton(1))
{
// 按下鼠标右键,旋转视角
Vector2 curMouse = Input.mousePosition;
CameraRotation(curMouse - lastMouse);
lastMouse = curMouse;
}
else
{
// 检测鼠标滚轮,计算新的摄像机轨道半径,进行缩放
float w = -Input.GetAxis("Mouse ScrollWheel");
distance = Mathf.Clamp(distance + w * ZoomRate, -MaxDistance, MaxDistance);
var trans = transform;
trans.position = trans.forward * distance;
}
break;
}
}
}
// 旋转摄像机
private void CameraRotation(Vector2 delta)
{
var trans = transform;
var rot = trans.localEulerAngles;
pitch = ClampAngle(pitch + delta.y * RotationRate, BottomClampAngle, TopClampAngle );
rot.x = pitch;
rot.y -= delta.x * RotationRate;
trans.localEulerAngles = rot;
trans.position = trans.forward * distance;
}
// 角度裁剪,保持-360~360之间。
private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
switch (lfAngle)
{
case < -360f:
lfAngle += 360f;
break;
case > 360f:
lfAngle -= 360f;
break;
}
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
}
private void LoadSceneData()
{
XmlElement node = Config.GetNode("Scenes");
if (node != null)
{
foreach (XmlNode child in node.ChildNodes)
{
if( child.NodeType != XmlNodeType.Element )
continue;
if( child is not XmlElement element )
continue;
if(string.Compare(element.Name, "Room", StringComparison.OrdinalIgnoreCase) != 0 )
continue;
string key = element.GetAttributeString("name");
if( string.IsNullOrWhiteSpace(key) || _rooms.ContainsKey(key))
continue;
string hdr = element.GetAttributeString("hdr");
if(!TextureFileLoader.IsPictureFile(hdr))
continue;
string thum = element.GetAttributeString("thum");
if(!TextureFileLoader.IsPictureFile(thum))
continue;
Texture2D htex = TextureFileLoader.GetTexture(hdr);
if(htex == null)
continue;
Texture2D ttex = TextureFileLoader.GetTexture(thum);
if( ttex==null)
continue;
RoomThumItem item = Instantiate(_thumItemPrefab, _roomListContent);
item.key = key;
item.texture = ttex;
GameObject indicator = null;
GameObject hotpoints = null;
foreach (XmlNode arrowsNode in element.ChildNodes)
{
if (arrowsNode.NodeType != XmlNodeType.Element)
continue;
if (arrowsNode is not XmlElement arrowElement)
continue;
if (string.Compare("Indicator", arrowElement.Name, StringComparison.OrdinalIgnoreCase) == 0)
{
string dir = arrowElement.GetAttributeString("type", "F").ToUpper();
Vector3 pos = arrowElement.GetAttribute("pos", new Vector3(-3, -2, 0));
var angle = arrowElement.GetAttribute("rot", 0f);
var enter = arrowElement.GetAttributeString("enter");
var arrow = dir switch
{
"F" => Instantiate(_forwordArrowPrefab),
"L" => Instantiate(_leftArrowPrefab),
"R" => Instantiate(_rightArrowPrefab),
_ => null
};
if (arrow != null)
{
if (indicator == null)
{
indicator = new GameObject($"R_{key}")
{
transform =
{
position = Vector3.zero
}
};
}
arrow.target = enter;
var arrTrans = arrow.transform;
arrTrans.position = pos;
arrTrans.eulerAngles = new Vector3(0, angle, 0);
arrTrans.SetParent(indicator.transform);
}
}
if (string.Compare("Hotpoint", arrowElement.Name, StringComparison.OrdinalIgnoreCase) == 0)
{
var hpos = arrowElement.GetAttribute("pos", Vector3.zero);
var url = arrowElement.GetAttributeString("url");
if (!TextureFileLoader.IsPictureFile(url) && !TextureFileLoader.IsVideoFile(url))
continue;
if (hotpoints == null)
{
hotpoints = Instantiate(_HPContentsPrefab, _hotpointsContent);
}
var hp = Instantiate(_hotPointPrefab, hotpoints.transform);
hp.title = arrowElement.GetAttributeString("title", "None Caption");
hp.worldPos = hpos;
hp.url = url;
}
}
if (indicator != null)
indicator.SetActive(false);
if(hotpoints != null)
hotpoints.SetActive(false);
_rooms.Add(key, new SceneNode()
{
texture = htex,
arrows = indicator,
hotpoints = hotpoints
});
}
}
return startNode;
}
配置文件如下:
<C360>
<StartPage rotaSpeed="10" rotaRate="2" />
<Camera rotaRate="0.1" zoomRate="-1" maxDistance="3" minAngle="-80" maxAngle="80" switchTime="1.5" />
<Scenes default="01">
<Room name="01" hdr="e:/temp/zt/01.jpg" thum="e:/temp/zt/t01.png">
<Indicator type="F" pos="-3,-0.9,-1.6" rot="-90" enter="02" />
Room>
<Room name="02" hdr="e:/temp/zt/02.jpg" thum="e:/temp/zt/t02.png">
<Indicator type="R" pos="0.12,-0.9,3.7" enter="03" />
<Hotpoint title="智慧城市" pos="5,0.9,0.2" url="e:/temp/back.mp4" />
<Hotpoint title="管廊百科" pos="1.4,0.5,-5" url="e:/temp/back.mp4" />
<Hotpoint title="发展历程" pos="-4,-0.8,-2.5" url="e:/temp/back.mp4" />
<Hotpoint title="政策指引" pos="-5,0.3,0.22" url="e:/temp/back.mp4" />
<Hotpoint title="建设意义" pos="-4,-0.8,2.8" url="e:/temp/back.mp4" />
Room>
<Room name="03" hdr="e:/temp/zt/03.jpg" thum="e:/temp/zt/t03.png">
<Indicator type="F" pos="-0.5,-1,4" enter="04" />
<Indicator type="R" pos="1,-1.6,4" enter="05" />
<Indicator type="F" pos="3,-3,-0.5" rot="90" enter="02" />
<Hotpoint title="经济便捷" pos="0.1,0.38,-5" url="e:/temp/back.mp4" />
<Hotpoint title="功能分级" pos="-4.5,0.3,-1" url="e:/temp/back.mp4" />
<Hotpoint title="先进技术" pos="-4,0.5,2.2" url="e:/temp/back.mp4" />
Room>
<Room name="04" hdr="e:/temp/zt/04.jpg" thum="e:/temp/zt/t04.png">
<Indicator type="F" pos="3.3,-3,-1.36" rot="90" enter="05" />
<Indicator type="L" pos="1.25,-1,-3.5" rot="180" enter="02" />
<Hotpoint title="先进技术" pos="-5,0.3,-4" url="e:/temp/back.mp4" />
<Hotpoint title="筑梦未来" pos="-4,0.3,3" url="e:/temp/back.mp4" />
Room>
<Room name="05" hdr="e:/temp/zt/05.jpg" thum="e:/temp/zt/t05.png">
<Indicator type="L" pos="0.3,-1,-4" rot="180" enter="03" />
<Hotpoint title="智慧运维" pos="-4,0,-0.2" url="e:/temp/back.mp4" />
Room>
Scenes>
C360>
读取XML文件的配置,已经封装好了一个类,请参看
Unity配置文件封装