Unity制作360全景图

Unity制作360全景图

天空盒还是球?

本来以为全景图很简单,把全景图纹理设置为“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;
}

配置文件如下:

<?xml version="1.0" encoding="utf-8"?>
<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配置文件封装

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
要实现360度展示片,可以使用以下步骤: 1. 准备一组 360 度全景片。 2. 在 HTML 中创建一个容器元素,例如 div,设置其宽度和高度。 3. 在该容器内创建一个 img 元素,设置其 src 属性为第一张全景片的路径。 4. 使用 JavaScript 监听鼠标移动事件,根据鼠标位置动态改变 img 元素的 src 属性,从而实现全景片的旋转。 以下是一个简单的示例代码: ```html <!DOCTYPE html> <html> <head> <title>360度全景片预览效果</title> <meta charset="utf-8"> <style> #container { width: 800px; height: 600px; overflow: hidden; position: relative; } #container img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transition: all 0.1s ease-in-out; } </style> </head> <body> <div id="container"> <img src="img/panorama1.jpg" alt="360度全景片"> </div> <script> var container = document.getElementById('container'); var img = container.getElementsByTagName('img')[0]; var total = 36; // 全景片总数 var current = 1; // 当前显示的片编号 var delta = 10; // 鼠标移动时每次旋转的角度 container.addEventListener('mousemove', function(event) { var x = event.clientX - container.offsetLeft; var y = event.clientY - container.offsetTop; var centerX = container.offsetWidth / 2; var centerY = container.offsetHeight / 2; var angleX = (x - centerX) * delta / centerX; var angleY = (y - centerY) * delta / centerY; img.style.transform = 'rotateX(' + angleY + 'deg) rotateY(' + angleX + 'deg)'; }); setInterval(function() { current++; if (current > total) { current = 1; } img.src = 'img/panorama' + current + '.jpg'; }, 100); </script> </body> </html> ``` 在上面的代码中,我们使用了 CSS 的 transform 属性来实现全景片的旋转效果。具体来说,我们通过修改 img 元素的 rotateX 和 rotateY 值来改变其旋转的角度,从而实现视角的转换。我们还使用了 JavaScript 的 setInterval 函数来定时切换全景片,以达到动态效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

示申○言舌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值