1 需求描述

        点选物体、框选物体、绘制外边框 中介绍了物体投影到屏幕上的二维外框绘制方法,本文将介绍物体外框线条盒子绘制方法。

内框:选中物体后,绘制物体的内框(紧贴物体、并与物体姿态一致的内框盒子)外框:选中物体后,绘制物体的外框(紧贴物体、并与世界坐标系的朝向一致的外框盒子)

        内框和外框效果如下,其中,黄色线框是内框,绿色线框是外框。

        本文完整代码见→Unity3D绘制物体外框线条盒子

2 需求实现

        1)原理

        获取物体外框盒子(Bounds)的方法主要有:

Bounds bounds = obj.GetComponent().mesh.bounds;

Bounds bounds = obj.GetComponent().bounds;

Bounds bounds = obj.GetComponent().bounds;

        MeshFilter、Render、Collider 获取的 Bounds 区别如下:

MeshFilter Bounds:模型原始 mesh 的 Bounds(局部坐标系下的坐标),在 Transform 组件中修改缩放,不会影响其值大小,还原其真实渲染值大小(世界坐标系下的坐标)需要通过transform.TransformPoint(vertex) 变换还原;Renderer Bounds:模型渲染的真实 Bounds(世界坐标系下的坐标),其姿态与世界坐标系的坐标轴朝向保持一致,在 Transform 组件中修改缩放,会影响其值大小;Collider Bounds:模型碰撞体的 Bounds(世界坐标系下的坐标),其姿态与世界坐标系的坐标轴朝向保持一致,在 Transform 组件中修改缩放,会影响其值大小,如果碰撞体与模型表面完全吻合,其 Bounds 与 Renderer 的 Bounds 保持一致。

        本文通过 MeshFilter Bounds 绘制内框盒子,通过 Renderer Bounds 绘制外框盒子。

        2)场景对象

        说明:需要删除 Plane 对象的碰撞体。 

        3)代码

        EventDetector.cs

using UnityEngine;

public class EventDetector : MonoBehaviour { // 事件检测器

private MyEventType eventType = MyEventType.None; // 事件类型

private MyEventType lastEventType = MyEventType.None; // 上次事件类型

private float scroll; // 滑轮滑动刻度

private bool detecting; // 事件检测中

private Vector3 clickDownMousePos; // 鼠标按下时的坐标

private const float dragThreshold = 1; // 识别为拖拽的鼠标偏移

private void Update() {

detecting = true;

DetectMouseEvent();

DetectScrollEvent();

UpgradeMouseEvent();

detecting = false;

lastEventType = eventType;

}

private void DetectMouseEvent() { // 检测鼠标事件

if (Input.GetMouseButtonDown(0)) { // Click Down

eventType = MyEventType.ClickDown;

clickDownMousePos = Input.mousePosition;

} else if (Input.GetMouseButtonUp(0)) {

if (IsDragEvent(eventType)) { // End Drag

eventType = MyEventType.EndDrag;

} else { // Click Up

eventType = MyEventType.ClickUp;

}

} else if (Input.GetMouseButton(0)) {

if (IsDragEvent(eventType)) { // Drag

eventType = MyEventType.Drag;

} else if (Vector3.Distance(clickDownMousePos, Input.mousePosition) > dragThreshold) { // Begin Drag

eventType = MyEventType.BeginDrag;

} else { // Click

eventType = MyEventType.Click;

}

} else {

eventType = MyEventType.None;

}

}

private void DetectScrollEvent() { // 检测滑轮事件

if (eventType != MyEventType.None

&& (!IsBeginEvent(eventType) || lastEventType != MyEventType.None && !IsScrollEvent(lastEventType))) {

scroll = 0;

return;

}

float temScroll = Input.GetAxis("Mouse ScrollWheel");

if (Mathf.Abs(scroll) < float.Epsilon && Mathf.Abs(temScroll) > float.Epsilon) { // Begin Scroll

eventType = MyEventType.BeginScroll;

scroll = temScroll;

} else if (Mathf.Abs(scroll) > float.Epsilon && Mathf.Abs(temScroll) < float.Epsilon) { // End Scroll

eventType = MyEventType.EndScroll;

scroll = temScroll;

} else if (Mathf.Abs(temScroll) > float.Epsilon) { // Scroll

eventType = MyEventType.Scroll;

scroll = temScroll;

} else {

scroll = 0;

}

}

private void UpgradeMouseEvent() { // 升级鼠标事件(关联键盘事件)

if (eventType == MyEventType.None) {

return;

}

if (IsBeginEvent(eventType)) {

if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) {

AddKeyType("Ctrl");

} else if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) {

AddKeyType("Alt");

}

} else {

ContinueKeyType(); // 保持按键事件

}

}

public MyEventType EventType() { // 事件类型

if (detecting) {

return lastEventType;

}

return eventType;

}

public bool HasClickEvent() { // 是否有点击事件

MyEventType type = EventType();

return IsClickEvent(type);

}

public bool HasDragEvent() { // 是否有拖拽事件

MyEventType type = EventType();

return IsDragEvent(type);

}

public bool HasScrollEvent() { // 是否有滑轮事件

MyEventType type = EventType();

return IsScrollEvent(type);

}

public bool HasCtrlScrollEvent() { // 是否有Ctrl滑轮事件

MyEventType type = EventType();

return type >= MyEventType.BeginCtrlScroll && type <= MyEventType.EndCtrlScroll;

}

public bool IsBeginDrag() { // 是否是开始拖拽类型事件

MyEventType type = EventType();

return type == MyEventType.BeginDrag || type == MyEventType.BeginCtrlDrag || type == MyEventType.BeginAltDrag;

}

public float Scroll() { // 鼠标滑轮滑动刻度

if (HasScrollEvent()) {

return scroll;

}

return 0;

}

private bool IsClickEvent(MyEventType type) { // 是否是点击事件

return type >= MyEventType.ClickDown && type <= MyEventType.CtrlClickUp;

}

private bool IsDragEvent(MyEventType type) { // 是否是拖拽事件

return type >= MyEventType.BeginDrag && type <= MyEventType.EndAltDrag;

}

private bool IsScrollEvent(MyEventType type) { // 是否是滑轮事件

return type >= MyEventType.BeginScroll && type <= MyEventType.EndCtrlScroll;

}

private bool IsBeginEvent(MyEventType type) { // 是否是开始类型事件

return type == MyEventType.ClickDown

|| type == MyEventType.BeginDrag

|| type == MyEventType.BeginCtrlDrag

|| type == MyEventType.BeginAltDrag

|| type == MyEventType.BeginScroll

|| type == MyEventType.BeginCtrlScroll;

}

private bool HasCtrlKey(MyEventType type) { // 是否有Ctrl按键事件

return type >= MyEventType.CtrlClickDown && type <= MyEventType.CtrlClickUp

|| type >= MyEventType.BeginCtrlDrag && type <= MyEventType.EndCtrlDrag

|| type >= MyEventType.BeginCtrlScroll && type <= MyEventType.EndCtrlScroll;

}

private bool HasAltKey(MyEventType type) { // 是否有Alt按键事件

return type >= MyEventType.BeginAltDrag && type <= MyEventType.EndAltDrag;

}

private void ContinueKeyType() { // 保持按键事件

if (HasCtrlKey(lastEventType)) {

AddKeyType("Ctrl");

} else if (HasAltKey(lastEventType)) {

AddKeyType("Alt");

}

}

private void AddKeyType(string key) { // 添加按键事件

if ("Ctrl".Equals(key)) {

if (eventType == MyEventType.ClickDown) { // 点击事件

eventType = MyEventType.CtrlClickDown;

} else if (eventType == MyEventType.Click) {

eventType = MyEventType.CtrlClick;

} else if (eventType == MyEventType.ClickUp) {

eventType = MyEventType.CtrlClickUp;

} else if (eventType == MyEventType.BeginDrag) { // 拖拽事件

eventType = MyEventType.BeginCtrlDrag;

} else if (eventType == MyEventType.Drag) {

eventType = MyEventType.CtrlDrag;

} else if (eventType == MyEventType.EndDrag) {

eventType = MyEventType.EndCtrlDrag;

} else if (eventType == MyEventType.BeginScroll) { // 滑轮事件

eventType = MyEventType.BeginCtrlScroll;

} else if (eventType == MyEventType.Scroll) {

eventType = MyEventType.CtrlScroll;

} else if (eventType == MyEventType.EndScroll) {

eventType = MyEventType.EndCtrlScroll;

}

} else if ("Alt".Equals(key)) {

if (eventType == MyEventType.BeginDrag) { // 拖拽事件

eventType = MyEventType.BeginAltDrag;

} else if (eventType == MyEventType.Drag) {

eventType = MyEventType.AltDrag;

} else if (eventType == MyEventType.EndDrag) {

eventType = MyEventType.EndAltDrag;

}

}

}

}

public enum MyEventType { // 事件类型

None = 0,

ClickDown = 1,

Click = 2,

ClickUp = 3,

CtrlClickDown = 4,

CtrlClick = 5,

CtrlClickUp = 6,

BeginDrag = 10,

Drag = 11,

EndDrag = 12,

BeginCtrlDrag = 13,

CtrlDrag = 14,

EndCtrlDrag = 15,

BeginAltDrag = 16,

AltDrag = 17,

EndAltDrag = 18,

BeginScroll = 20,

Scroll = 21,

EndScroll = 22,

BeginCtrlScroll = 23,

CtrlScroll = 24,

EndCtrlScroll = 25

}

        说明: EventDetector 脚本组件挂在相机下,用于统一管理事件。点选物体(ClickUp)、滑动选框(Drag)、场景变换(Ctrl + Drag / Alt + Drag)都有鼠标事件,这些事件相互冲突,不便于在每个类里都去捕获鼠标和键盘事件,因此需要 EventDetector 统一管理事件。

        ClickSelect.cs

using UnityEngine;

public class ClickSelect : MonoBehaviour {

private EventDetector eventDetector; // 鼠标事件检测器

private LineBoxPainder lineBoxPainder;

private Transform target; // 选中的目标

private RaycastHit hit; // 碰撞信息

private void Awake() {

eventDetector = Camera.main.GetComponent();

lineBoxPainder = LineBoxPainder.GetInstance();

}

private void Update() {

if (eventDetector.EventType() == MyEventType.ClickUp) {

Transform temp = GetHitTrans();

UpdateColor(target, temp);

target = temp;

if (target != null) {

lineBoxPainder.DrawLineBox(target.gameObject);

}

else {

lineBoxPainder.DrawLineBox(null);

}

}

}

private void UpdateColor(Transform old, Transform now) { // 更新颜色

if (old != now) {

if (old != null) {

old.GetComponent().material.color = Color.gray;

}

if (now != null) {

now.GetComponent().material.color = Color.red;

}

}

}

private Transform GetHitTrans() { // 获取屏幕射线碰撞的物体

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

if (Physics.Raycast(ray, out hit)) {

return hit.transform;

}

return null;

}

}

        说明:ClickSelect 脚本组件挂在 Work 下。

        LineBoxPainder.cs

using UnityEngine;

public class LineBoxPainder { // 线段盒子渲染器(每个线段盒子由4个矩形组成)

private static LineBoxPainder instance; // 单例

private GameObject lineParent; // 线条盒子父对象

private LineRenderer[][] lineRenderers; // 线段渲染器

private Material lineMaterial; // 线段材质

private LineBoxPainder() {

lineMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));

lineParent = new GameObject("LineBoxPainder");

lineRenderers = new LineRenderer[2][];

lineRenderers[0] = GetLineRenderers("InnerBox", Color.yellow);

lineRenderers[1] = GetLineRenderers("OuterBox", Color.green);

}

public static LineBoxPainder GetInstance() { // 获取单例

if (instance == null) {

instance = new LineBoxPainder();

}

return instance;

}

public void DrawLineBox(GameObject obj) { // 绘制内框盒子和外框盒子

Vector3[] InnerVertices = BoxProvider.GetInnerBox(obj);

DrawBox(lineRenderers[0], InnerVertices); // 绘制内框盒子

Vector3[] OuterVertices = BoxProvider.GetOuterBox(obj);

DrawBox(lineRenderers[1], OuterVertices); // 绘制外框盒子

}

private LineRenderer[] GetLineRenderers(string name, Color color) { // 获取线段渲染器

Material material = new Material(lineMaterial);

material.color = color;

GameObject box = new GameObject(name);

box.transform.parent = lineParent.transform;

LineRenderer[] lines = new LineRenderer[4];

for (int i = 0; i < lines.Length; i++) {

GameObject line = new GameObject("Line" + i);

line.transform.parent = box.transform;

lines[i] = line.AddComponent();

lines[i].material = material;

lines[i].textureMode = LineTextureMode.Tile;

lines[i].widthMultiplier = 0.2f;

lines[i].startWidth = 0.05f;

lines[i].endWidth = 0.05f;

lines[i].positionCount = 0;

lines[i].loop = true;

}

return lines;

}

private void DrawBox(LineRenderer[] lines, Vector3[] vertices) { // 绘制一个长方体线段盒子, 每个盒子由4个矩形组成

if (vertices == null || vertices.Length == 0) {

for (int i = 0; i < 4; i++) { // 清空线段顶点

lines[i].positionCount = 0;

}

return;

}

else

{

for (int i = 0; i < 4; i++) { // 初始化线段顶点

lines[i].positionCount = 4;

}

}

for (int i = 0; i < 4; i++) { // 计算每个矩形的顶点序列

lines[0].SetPosition(i, vertices[i]);

lines[1].SetPosition(i, vertices[i + 4]);

if (i < 2) {

lines[2].SetPosition(i, vertices[i]);

lines[3].SetPosition(i, vertices[i + 2]);

} else {

lines[2].SetPosition(i, vertices[7 -i]);

lines[3].SetPosition(i, vertices[9 -i]);

}

}

}

}

        说明:LineBoxPainder 用于绘制内框和外框线段盒子,每个盒子使用 4 个 LineRenderer 渲染(对应 4 个矩形),每个 LineRenderer 有 4 个顶点,并设置为 loop,用于渲染一个矩形,一个长方体需要 4 个矩形拼成。

        BoxProvider.cs

using UnityEngine;

public class BoxProvider { // 盒子提供者

public static Vector3[] GetInnerBox(GameObject obj) { // 获取内框盒子8个顶点数据

if (obj == null || obj.GetComponent() == null) {

return null;

}

Bounds bounds = obj.GetComponent().mesh.bounds;

Vector3[] vertices = GetBoxVertices(bounds);

for (int i = 0; i < vertices.Length; i++) { // 将局部坐标转换为世界坐标

vertices[i] = obj.transform.TransformPoint(vertices[i]);

}

return vertices;

}

public static Vector3[] GetOuterBox(GameObject obj) { // 获取外框盒子8个顶点数据

if (obj == null || obj.GetComponent() == null) {

return null;

}

Bounds bounds = obj.GetComponent().bounds;

//Bounds bounds = obj.GetComponent().bounds;

Vector3[] vertices = GetBoxVertices(bounds);

return vertices;

}

private static Vector3[] GetBoxVertices(Bounds bounds) { // 根据中心坐标和半边长计算8个顶点的数据

Vector3 center = bounds.center;

Vector3 extents = bounds.extents;

Vector3[] vertices = new Vector3[8];

vertices[0] = center + new Vector3(extents.x, extents.y, extents.z);

vertices[1] = center + new Vector3(extents.x, extents.y, -extents.z);

vertices[2] = center + new Vector3(extents.x, -extents.y, -extents.z);

vertices[3] = center + new Vector3(extents.x, -extents.y, extents.z);

vertices[4] = center + new Vector3(-extents.x, extents.y, extents.z);

vertices[5] = center + new Vector3(-extents.x, extents.y, -extents.z);

vertices[6] = center + new Vector3(-extents.x, -extents.y, -extents.z);

vertices[7] = center + new Vector3(-extents.x, -extents.y, extents.z);

return vertices;

}

}

        说明:BoxProvider 用于计算物体外框的 8 个顶点序列。本文通过 MeshFilter Bounds 绘制内框盒子,通过 Renderer Bounds 绘制外框盒子。

        SceneController.cs

using System;

using UnityEngine;

public class SceneController : MonoBehaviour { // 场景变换控制器

private EventDetector eventDetector; // 鼠标事件检测器

public Action camChangedHandler; // 相机改变处理器

private Transform cam; // 相机

private float nearPlan; // 近平面

private Vector3 preMousePos; // 上一帧的鼠标坐标

private void Awake() {

cam = Camera.main.transform;

nearPlan = Camera.main.nearClipPlane;

eventDetector = cam.GetComponent();

}

private void Update() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)

if (eventDetector.HasCtrlScrollEvent()) { // 缩放场景

ScaleScene(eventDetector.Scroll());

} else if (eventDetector.IsBeginDrag()) {

preMousePos = Input.mousePosition;

} else if (eventDetector.HasDragEvent()) {

Vector3 offset = Input.mousePosition - preMousePos;

if (eventDetector.EventType() == MyEventType.CtrlDrag) { // 移动场景

MoveScene(offset);

} else if (eventDetector.EventType() == MyEventType.AltDrag) { // 旋转场景

RotateScene(offset);

}

preMousePos = Input.mousePosition;

}

}

private void ScaleScene(float scroll) { // 缩放场景

cam.position += cam.forward * scroll;

camChangedHandler?.Invoke();

}

private void MoveScene(Vector3 offset) { // 平移场景

cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);

camChangedHandler?.Invoke();

}

private void RotateScene(Vector3 offset) { // 旋转场景

Vector3 rotateCenter = GetRotateCenter(0);

cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量

cam.LookAt(rotateCenter);

cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 竖直拖拽分量

camChangedHandler?.Invoke();

}

private Vector3 GetRotateCenter(float planeY) { // 获取旋转中心

if (Mathf.Abs(cam.forward.y) < Vector3.kEpsilon || Mathf.Abs(cam.position.y) < float.Epsilon)

{

return cam.position + cam.forward * (nearPlan + 1 / nearPlan);

}

float t = (planeY - cam.position.y) / cam.forward.y;

float x = cam.position.x + t * cam.forward.x;

float z = cam.position.z + t * cam.forward.z;

return new Vector3(x, planeY, z);

}

}

        SceneController 脚本组件挂在相机下,用于平移、旋转、缩放场景,其原理见→缩放、平移、旋转场景。

3 运行效果

4 拓展

        本节主要介绍长方体盒子的信息解析,主要解决以下 2 个问题:

已知长方体顶点坐标,求长方体中心坐标、尺寸、旋转角度; 已知长方体中心坐标、尺寸、旋转角度,求长方体顶点坐标。

        BoxParser.cs

using UnityEngine;

public class BoxParser { // 解析盒子信息

// 已知长方体顶点坐标, 获取长方体中心坐标、尺寸、旋转角度

// 输入的顶点顺序: 右上前、右上后、右下后、右下前、左上前、左上后、左下后、左下前

public static void GetBoxInfo(Vector3[] vertices, out Vector3 center, out Vector3 extents, out Vector3 rotation) {

center = (vertices[0] + vertices[6]) / 2;

float sizeX = Vector3.Distance(vertices[0], vertices[4]); // 上前棱长

float sizeY = Vector3.Distance(vertices[0], vertices[3]); // 右前棱长

float sizeZ = Vector3.Distance(vertices[0], vertices[1]); // 右上棱长

extents = new Vector3(sizeX, sizeY, sizeZ) / 2;

Vector3 forward = (vertices[0] + vertices[7]) / 2 - center; // 本地向前的向量

Vector3 up = (vertices[0] + vertices[5]) / 2 - center; // 本地向上的向量

Quaternion qua = Quaternion.LookRotation(forward, up);

rotation = qua.eulerAngles;

}

// 已知长方体中心坐标、尺寸、旋转角度, 获取长方体顶点坐标

public static Vector3[] GetVertices(Vector3 center, Vector3 extents, Vector3 rotation) {

Vector3[] vertices = GetInitVertices(extents);

RotateAndTranslate(vertices, rotation, center);

return vertices;

}

private static Vector3[] GetInitVertices(Vector3 extents) { // 根据半边长计算8个顶点的数据

// 输出的顶点顺序: 右上前、右上后、右下后、右下前、左上前、左上后、左下后、左下前

Vector3[] vertices = new Vector3[8];

vertices[0] = new Vector3(extents.x, extents.y, extents.z);

vertices[1] = new Vector3(extents.x, extents.y, -extents.z);

vertices[2] = new Vector3(extents.x, -extents.y, -extents.z);

vertices[3] = new Vector3(extents.x, -extents.y, extents.z);

vertices[4] = new Vector3(-extents.x, extents.y, extents.z);

vertices[5] = new Vector3(-extents.x, extents.y, -extents.z);

vertices[6] = new Vector3(-extents.x, -extents.y, -extents.z);

vertices[7] = new Vector3(-extents.x, -extents.y, extents.z);

return vertices;

}

private static void RotateAndTranslate(Vector3[] vertices, Vector3 rotation, Vector3 center) { // 旋转和平移

Quaternion qua = Quaternion.Euler(rotation.x, rotation.y, rotation.z);

Matrix4x4 matrix = Matrix4x4.Rotate(qua);

for (int i = 0; i < vertices.Length; i++) { // 将局部坐标转换为世界坐标

vertices[i] = center + matrix.MultiplyPoint(vertices[i]);

}

}

}

查看原文