基于正二十面体球面网格的三角形四叉树星球 LOD 地形系统设计与实现(番外)

前情提要

在上一篇文章中,读者应该能看到scene窗口的神奇情况 这样的

这样的

诶,怎么摄像机歪了?是unity自己抽风了,累计了太多的浮点误差导致摄像机歪了吗?

非也非也,为了方便开发太空主题的游戏,自然需要一个6自由度的Scene窗口的相机,否则在scene窗口内在星球表面飞行摄像机总是会相对于星球的局部平面"歪掉",还是挺麻烦的。下面我写的脚本晕3d的人慎用。

效果演示 && 操作指南

默认情况下:wasd移动,空格上升,c下降

6自由度模式:wasd移动,qe滚转

按住alt+鼠标左键拖动,围绕摄像机的Pivot旋转,和unity默认scene窗口功能一样,但是可以在6自由度和默认之间切换

按住alt+鼠标中键拖动,围绕当前摄像机的z轴旋转

ctrl+1,重置摄像机的上方向为默认,回正摄像机的roll

ctrl+2,设置摄像机的y轴方向为当前上方向

ctrl+3,解锁/锁定摄像机roll

实现原理:

(1)拦截unity scene窗口的输入 SceneView.duringSceneGui+=一个接受镜头控制输入的函数,通过 Event e = Event.current拦截unity在scene窗口内的输入,处理完输入信息后使用e.Use()吃掉该事件,这样unity就不会自己处理这个输入事件了,把输入的信息保存在静态类内部,这个事件的更新时间是不固定的,如果在这个函数内实现摄像机旋转和移动,就会一卡一卡的,笔者已经踩过坑了。 SceneView.update+=一个每帧更新的函数,处理上面接收到的输入信息,

(2)scene窗口的摄像机有一个Pivot,可以把pivot想象成拿自拍杆的手,对 sceneView.rotation的修改可以很方便地让摄像机的围绕Pivot旋转,但是要如何实现右键围绕摄像机自身旋转呢?很粗暴的想法就是,既然scene窗口每一次旋转之后,摄像机的位置都会偏离到另一个地方,那我通过改变pivot强行把摄像机拉回到原来的位置,不就行了?我不确定unity内部默认的右键旋转功能是不是这么实现的,但是scene窗口里的能用就行。

C# 复制代码
Vector2 delta = e.mousePosition - lastMousePos;
lastMousePos = e.mousePosition;
//---------绕着场景摄像机自身的x、y轴旋转--------------
Quaternion rotToPivot = sceneView.rotation;
Vector3 camPos = sceneView.camera.transform.position;
Vector3 pivotPos = sceneView.pivot;
// float distance = sceneView.cameraDistance;
// 改变相机看向pivot的方向
Quaternion deltaRot;
if (lockZAxis)
{
    deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right) 
        * Quaternion.AngleAxis(delta.x * 0.2f, currUpDir);
}
else
{
    deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right) 
        * Quaternion.AngleAxis(delta.x * 0.2f, sceneView.camera.transform.up);
}
Quaternion newRotToPivot = deltaRot * rotToPivot;
// 计算原本相机在旋转后会到达的位置
Vector3 newCamPos = pivotPos + deltaRot * (camPos - pivotPos);

//相应地改变pivot的位置,使得相机本身的位置保持不变
Vector3 newPivot = pivotPos - (newCamPos - camPos);
sceneView.rotation = newRotToPivot;
sceneView.pivot = newPivot;

完整代码

ini 复制代码
#define USE_CUSTOM_SCENE_MOVE_CONTROL
#define USE_EXP_SPEED

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[InitializeOnLoad]
public static class SceneViewRoller
{
#if USE_CUSTOM_SCENE_MOVE_CONTROL
    static bool lockZAxis = false; //锁定摄像机的Z轴的时候用于平面编辑,如果解锁,则适合用于星球表面的编辑
    
    //当前摄像机的上方向向量,可以在运行时更改
    static Vector3 currUpDir = Vector3.up; 

    static readonly float shift_acceleration = 4f; //按下LeftShift键时的加速度倍数
    static readonly Dictionary<KeyCode, bool> keyPressMap = new Dictionary<KeyCode, bool>()
    {
        { KeyCode.W, false },//是否按下了W键
        { KeyCode.S, false },//是否按下了S键
        { KeyCode.A, false },//是否按下了A键
        { KeyCode.D, false },//是否按下了D键
        { KeyCode.Q, false },//是否按下了Q键,用于向左滚转
        { KeyCode.E, false },//是否按下了E键,用于向右滚转
        { KeyCode.Space, false },//是否按下了Space键,用于上升
        { KeyCode.C, false },//是否按下了C键,用于下降
        { KeyCode.LeftShift, false },//是否按下了LeftShift键,用于加速
    };

    static readonly float rollSpeed = 40f;
    static bool isMoving = false;
    static Vector2 lastMousePos;
    
    static readonly float moveAcceleration = 10f;
    static readonly float MOVE_SPEED_LIMIT = 200f;
    static readonly float speedAtten = 0.5f;
    static Vector3 moveVelocity = Vector3.zero;


    static SceneViewRoller()
    {
        last_second = Time.realtimeSinceStartup;
        // 每帧刷新
        SceneView.duringSceneGui += OnSceneGUI;
        EditorApplication.update += OnUpdate;
    }

    static double last_second;
    static float holdTime = 0f;
    private static void OnUpdate()
    {
        double this_second = EditorApplication.timeSinceStartup;
        double realDeltaTimeD = (this_second - last_second);
        
        float realDeltaTime = (float)(realDeltaTimeD);

        last_second = this_second;
        //Debug.Log("Editor is updating,");
        
        var sceneView = SceneView.lastActiveSceneView;

        //根据WASD键计算加速的方向
        Vector3 moveDir = Vector3.zero;
        bool is_wasd = false;
        foreach(var key in keyPressMap.Keys)
        {
            if(keyPressMap[key])
            {
                is_wasd = true;
                switch(key)
                {
                    case KeyCode.W:
                        moveDir += sceneView.camera.transform.forward;
                        break;
                    case KeyCode.S:
                        moveDir -= sceneView.camera.transform.forward;
                        break;
                    case KeyCode.A:
                        moveDir -= sceneView.camera.transform.right;
                        break;
                    case KeyCode.D:
                        moveDir += sceneView.camera.transform.right;
                        break;
                    case KeyCode.Space:
                        moveDir += sceneView.camera.transform.up;
                        break;
                    case KeyCode.C:
                        moveDir -= sceneView.camera.transform.up;
                        break;
                    case KeyCode.Q:
                        if(!lockZAxis)
                        {//如果没有锁定Z轴,才允许滚转,否则会导致万向节死锁
                            sceneView.rotation = Quaternion.AngleAxis(rollSpeed * realDeltaTime, sceneView.camera.transform.forward) * sceneView.rotation;
                        }
                        break;
                    case KeyCode.E:
                        if(!lockZAxis)
                        {
                            sceneView.rotation = Quaternion.AngleAxis(-rollSpeed * realDeltaTime, sceneView.camera.transform.forward) * sceneView.rotation;
                        }
                        break;
                }
            }
        }
        moveDir = moveDir.normalized;
        if(!is_wasd)
        {
            holdTime = 0f;
            //计算速度衰减
            moveVelocity *= speedAtten;
            if(moveVelocity.magnitude < 0.01f)
            {
                return;
            }
            sceneView.pivot += moveVelocity * realDeltaTime;
        }
        else
        {
            holdTime += realDeltaTime;
            //根据moveDir和加速度计算新的速度
            var delta_v = moveAcceleration * realDeltaTime * moveDir;
            // Debug.Log("delta_v: " + delta_v);
            // moveVelocity += delta_v;
            moveVelocity = moveVelocity.magnitude * moveDir + delta_v * ((moveVelocity.magnitude < 1E-5f) ? 1f : Vector3.Dot(moveDir, moveVelocity.normalized));
            moveVelocity = Vector3.ClampMagnitude(moveVelocity, MOVE_SPEED_LIMIT);

#if USE_EXP_SPEED
            // 指数增长速度
            float k = 0.1f;
            float speed = MOVE_SPEED_LIMIT * (1f - Mathf.Exp(-k * holdTime));
            sceneView.pivot += (keyPressMap[KeyCode.LeftShift] ? shift_acceleration : 1f) * speed * realDeltaTime * moveDir;
#else
            //根据moveVelocity计算pivot移动位置
            // Debug.Log("moveVelocity: " + moveVelocity);
            sceneView.pivot += (keyPressMap[KeyCode.LeftShift] ? shift_acceleration : 1f) * realDeltaTime * moveVelocity;
#endif
        }
        
    }

    private static void OnSceneGUI(SceneView sceneView)
    {
        Event e = Event.current;
        // Rect sceneRect = sceneView.position; // SceneView 的矩形区域

        //=========================锁定Z轴相关=========================
        if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha1)
        {//ctrl + 1,重置当前摄像机上方向为Vector3.up,并同时把摄像机摆正。 如果不摆正的话,会导致摄像机的Z轴和场景中的Z轴不一致,当摄像机的欧拉角的Z轴为90度时,甚至会导致万向节死锁
            Quaternion rot = sceneView.rotation;
            rot.eulerAngles = new Vector3(rot.eulerAngles.x, rot.eulerAngles.y, 0f);
            sceneView.rotation = rot;
            currUpDir = Vector3.up;
            e.Use(); // 吃掉事件
            SceneViewMsg("重置当前摄像机上方向为Vector3.up,回正摄像机Z轴");
        }
        if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha2)
        {//ctrl + 2,设置currUpDir为sceneView.camera.transform.up
            currUpDir = sceneView.camera.transform.up;
            lockZAxis = true;
            e.Use(); // 吃掉事件
            SceneViewMsg("设置currUpDir为摄像机当前的上方向,并把Z轴锁定");
        }
        if (e.control && e.type == EventType.KeyDown && e.keyCode == KeyCode.Alpha3)
        {//ctrl + 3,开关lockZAxis
            lockZAxis = !lockZAxis;
            e.Use(); // 吃掉事件
            SceneViewMsg(lockZAxis ? "Z轴现在被锁定" : "Z轴现在被解锁");
        }

        //=========================场景中自由移动相关=========================

        // 开始右键自由移动
        if (e.type == EventType.MouseDown && e.button == 1)
        {
            isMoving = true;
            lastMousePos = e.mousePosition;
            e.Use();   // 阻止 Unity 默认右键旋转

            // Cursor.lockState = CursorLockMode.Locked;
            // Cursor.visible = false;
        }
        // 停止右键自由移动
        if (e.type == EventType.MouseUp && e.button == 1)
        {
            isMoving = false;
            e.Use();
            UnableAllButton();

            // Cursor.lockState = CursorLockMode.Locked;
            // Cursor.visible = true;
        }
        // 自定义右键转动摄像机的逻辑
        if (isMoving && e.type == EventType.MouseDrag && e.button == 1)
        {
            // EditorGUIUtility.AddCursorRect(sceneRect, MouseCursor.MoveArrow);

            Vector2 delta = e.mousePosition - lastMousePos;
            lastMousePos = e.mousePosition;

            //---------绕着场景摄像机自身的x、y轴旋转--------------
            Quaternion rotToPivot = sceneView.rotation;
            Vector3 camPos = sceneView.camera.transform.position;
            Vector3 pivotPos = sceneView.pivot;
            // float distance = sceneView.cameraDistance;
            // 改变相机看向pivot的方向
            Quaternion deltaRot;
            if (lockZAxis)
            {
                deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right) 
                    * Quaternion.AngleAxis(delta.x * 0.2f, currUpDir);
            }
            else
            {
                deltaRot = Quaternion.AngleAxis(delta.y * 0.2f, sceneView.camera.transform.right) 
                    * Quaternion.AngleAxis(delta.x * 0.2f, sceneView.camera.transform.up);
            }
            Quaternion newRotToPivot = deltaRot * rotToPivot;

            // 计算原本相机在旋转后会到达的位置
            Vector3 newCamPos = pivotPos + deltaRot * (camPos - pivotPos);

            //相应的改变pivot的位置,使得相机本身的位置保持不变
            Vector3 newPivot = pivotPos - (newCamPos - camPos);
            // Debug.LogFormat("newCamPos: {0}", newCamPos);

            sceneView.rotation = newRotToPivot;
            sceneView.pivot = newPivot;

            e.Use();
            
             //sceneView.Repaint();
        }
        //记录键盘输入,将在Update中用于自由移动
        if (isMoving && keyPressMap.ContainsKey(e.keyCode))
        {
            bool isKeyDown = e.type == EventType.KeyDown;
            // Debug.LogFormat("按下{0}键",e.keyCode.ToString());
            keyPressMap[e.keyCode] = isKeyDown;
            e.Use();
        }
    
        //=========================自定义Alt旋转相关=========================
        //开始alt旋转
        if (e.alt && e.type == EventType.MouseDown && e.button == 0)
        {
            lastMousePos = e.mousePosition;
            e.Use();
        }
        //alt旋转中
        if (e.alt && e.type == EventType.MouseDrag && e.button == 0)
        {
            Vector2 delta_mouse = e.mousePosition - lastMousePos;
            lastMousePos = e.mousePosition;

            if (lockZAxis)
            {//在Z轴锁定的状态下:alt旋转围绕pivot旋转时,以currUpDir为北极方向
                Quaternion rotToPivot = sceneView.rotation;
                Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse.x * 0.2f, currUpDir) 
                    * Quaternion.AngleAxis(delta_mouse.y * 0.2f, sceneView.camera.transform.right);
                Quaternion newRotToPivot = deltaRot * rotToPivot;
                sceneView.rotation = newRotToPivot;
            }
            else
            {//在Z轴解锁的状态下:自由旋转,类似戴森球计划里的星球旋转方式,可以通过Q和E旋转摄像机的Z轴
                Quaternion rotToPivot = sceneView.rotation;
                Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse.x * 0.2f, sceneView.camera.transform.up) 
                    * Quaternion.AngleAxis(delta_mouse.y * 0.2f, sceneView.camera.transform.right);
                Quaternion newRotToPivot = deltaRot * rotToPivot;
                sceneView.rotation = newRotToPivot;
            }


            e.Use();
        }
        //alt+鼠标中键按下拖拽调整摄像机Z轴
        if (e.alt && e.type == EventType.MouseDown && e.button == 2)
        {
            lastMousePos = e.mousePosition;
            e.Use();
        }
        if (e.alt && e.type == EventType.MouseDrag && e.button == 2)
        {
            float delta_mouse = (e.mousePosition - lastMousePos).x;
            lastMousePos = e.mousePosition;
            //调整摄像机的Z轴方向
            Quaternion rotToPivot = sceneView.rotation;
            Quaternion deltaRot = Quaternion.AngleAxis(delta_mouse * 0.2f, sceneView.camera.transform.forward);
            Quaternion newRotToPivot = deltaRot * rotToPivot;
            sceneView.rotation = newRotToPivot;
            e.Use();
        }
    }


    static void UnableAllButton()
    {
        // 创建键的副本以避免在枚举时修改集合
        List<KeyCode> keys = new List<KeyCode>(keyPressMap.Keys);
        foreach(var key in keys)
        {
            keyPressMap[key] = false;
        }
    }

#endif

    private static void SceneViewMsg(string msg)
    {
        Debug.Log("SCENE_VIEW_LOG:"+msg);
    }
}

完整代码

IcoSphereTerrainLODSystem: 实现了基于20面体球面网格的三角形四叉树细分星球LOD地形系统。

参考

GPU驱动的四叉树地形以及参考了这个文章的代码

  1. (49 封私信 / 70 条消息) 大世界GPU Driven地形入门 - 知乎

地形的噪声生成、海洋和大气层渲染参考了这个仓库的代码

  1. SebLague/Solar-System at Episode_02 (MIT)
相关推荐
fetasty1 天前
Godot游戏练习01-第5节-游戏显示与像素资源
游戏开发
SmalBox1 天前
【节点】[SceneColor节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[Object节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[Fog节点]原理解析与实际应用
unity3d·游戏开发·图形学
fetasty4 天前
Godot游戏练习01-第3节-多人场景创建
游戏开发
开维游戏引擎4 天前
开维游戏引擎实例:AI自动生成游戏代码:飞翔的小鸟FlappyBird
ai编程·游戏开发
SmalBox4 天前
【节点】[EyeIndex节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox5 天前
【节点】[DepthFade节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox6 天前
【节点】[Camera节点]原理解析与实际应用
unity3d·游戏开发·图形学