Unity的实时绘制与坐标转换总结

发表于2015-10-28
评论0 4.7k浏览

    最近在搞有关Unity的交互和线路绘制的问题,在这里总结一下。

    比如说:我们需要在屏幕上用鼠标拖动,然后屏幕上面会根据我们的动作动态绘制出线路,可以是直线,点,或者抛物线等等。这里主要的问题有两个,第一:是采用什么方法来绘制,第二,采用什么坐标来绘制。

    首先我们绘制一根线(或者点集)的话,线的绘制可以采用的方式有:

1.LineRender,

2.NGUI绘制

3.直接使用Unity内置的基本几何图形(cube ,sphere等)做点集合,

4.使用GL这个底层库等等方式。

    根据不同的绘制方式,我们需要搞清楚它们所使用的坐标,这里是个很重要的问题,涉及多种坐标的转换问题,Unity本身有这么几种坐标:

   1.世界坐标:普通三维世界使用,原点在屏幕中间,比如说:XXobject.transform.position

   2.屏幕坐标:鼠标使用(移动平台的手指触控也是),和屏幕分辨率有关,原点在屏幕左下角,如:Input.mousePosition

   3.投影坐标:和摄像机投影变换有关,用户使用比较少

   4.GUI坐标,Unity内置GUI使用,单位是像素和屏幕坐标一样,但是原点在左上角

下面说说具体使用的方式:

使用LineRenderer :

    如果说我们选择绘制线路的方式是LineRender,由于其使用的是世界坐标,假如说我们要实现在屏幕上面点一下,然后就在那个点放置我们的小球。那么我们的步骤大致是:1.捕获鼠标点击的时候的屏幕坐标,2.把屏幕坐标转换为世界坐标。3.把世界坐标赋值给小球。

1. 获取鼠标的屏幕坐标的方法是Vector pos = Input.mousePosition,

2.坐标转换的方法是Camera.main.ScreenToWorldPoint(pos), 但是!,这里有一个非常重要的点要注意,就是由于鼠标的坐标是2d(屏幕坐标),z轴是有问题的,因为屏幕上面的一点对应的的是空间上面的一条射线(如下图,透视投影和正交投影)。

 

    无论是透视投影还是正交投影。所以直接转换过去的世界坐标是很有问题的,z轴不对。经过实践,Unity里面的Camera.main.ScreenToWorld方法,并不会直接能把获取到的屏幕坐标(如:Vector3 A)转成正确的世界坐标。这里我们要先在转成世界坐标之前给它的z轴赋一个有意义的值。如果转换之后再赋值会出错的。你们可以简单的把pos.z = 1;这样赋予正确的值。这里介绍我的做法:由于我知道了我的点的z轴坐标需要和起点一致,所以我先取得起点的世界坐标(如Vector3 B =_vecStart ),转成屏幕坐标Vector C = Camera.main.ScreenToWorldPoint(B),再拿出z轴赋给我们要处理的坐标(A.z= C.z),这样A转换过后的z轴和起点_vecStart一致了。此时A的三个坐标的是有意义的,这时对A进行转换才能达到我们的预期。

坐标的转换才是关键,做完了转换的话,直接赋值给绘制的方法就好了,LineRender的具体使用方法是:

LineRenderer _lineRenderer; 

_lineRenderer = gameObject.GetComponent<LineRenderer>();// 需要在控件上挂LineRenderer组件,在这里取得引用

_lineRenderer.SetWidth(0.3f, 0.3f);  //设置起点和终点的线框大小

_lineRenderer.SetVertexCount(10); //设置点的个数

_lineRenderer.useWorldSpace = true; // 使用世界坐标

for (int j = 0; j < 10; j++)  //指点每个点的坐标,世界坐标,j是点的编号,从零开始

{_lineRenderer.SetPosition(j, new Vector3(0, 0, 0));

}

 

    另外,在inspector面板,还可以可视化的指定材质,点的个数,等等。材质可以设成颜色,这样不太美观(如左下图),设贴图的话在比较段的线还不错,如果设置的点数比较多的话,采用贴图很有问题,贴图被拉伸。如果要绘制长线的话只能采取多个LineRenderer,首尾连接,这样又会变得非常的麻烦。

使用NGUI :

    采取NGUI这个比较成熟的插件的话,绘制点线的话,图元的制作会变得非常的简单,主要问题是坐标的问题。由于NGUI采用的是却别于Unity的几大坐标,使用自己独有的一套坐标,因此需要在进行一次转换,至于Unity和NGUI的对接点在屏幕坐标,因为NGUI的几个转换坐标的方法中ScreenToWorldPoint(vecTempScreen)这个方法很像Unity 的那个,这里转换后的世界坐标是NGUI的世界坐标,转换前的屏幕坐标就是Unity的屏幕坐标,所以可以用屏幕坐标变中介。

好了,具体的做法是这样子的,如果是Unity的场景中一个物体Cube,要把它的世界坐标转成NGUI坐标,我们可以这么做:

Vector3 pos = Cube.transform.position;
Vector3 posScreen = Camera.main.WorldToScreenPoint(pos); //这里的Camera.main指的是世界坐标场景中的摄像机

Vector posNgui = UICamera.currentCamera.ScreenToWorldPoint(posScreen); //这里的currentCamera指的是NGUI场景中的摄像机,一般在UIRoot的子节点下

左下图为上面代码中的摄像机参数的可视化显示,中下图为运行效果,右下图为NGUI的坐标和世界坐标的对比(白色为世界坐标,红框里面为NGUI内容)

    这里要提一点,有时候Unity会提示 UICamera.currentCamera取不到实例,这个极有可能是场景中有好几台摄像机(最基本的世界场景一台,NGUI一台,背景和背包各用一台摄像机是很正常的,所以场景中的摄像机比较多的情况下,UICamera.currentCamera取不到我们想要的那台摄像机,所以自己直接找到那台拖拽赋值是最保险的),我们可以自己定义一个变量public Camera NguiCurrentCamera,直接在Inspector界面中把UIRoot中的摄像机拖给它,这样不会为空了。这么使用就可以NguiCurrentCamera.ScreenToWorldPoint(posScreen);接下来,就可以直接把posNgui赋值给NGUI的图元的坐标了,比如说:NGUIGameObject.transform.position = posNgui.

注意:

如果是直接获取屏幕坐标(比如说鼠标点击),转为NGUI世界坐标,我们很容易想到直接把上面的步骤拿来用,忽略第一步即可,思路是这样的。但是不要忘记,还是那个问题,屏幕坐标的Z轴是有问题的,所以应该先把Z轴赋予正确的值,比如说1,再进行转换:

Vector3 posScreen = Input.mousePosition;

posScreen.z = 1;

Vector  posNgui= UICamera.currentCamera.ScreenToWorldPoint(posScreen);

这样posNgui就可以使用了。

另外提一点,由于世界坐标相对来说比较小,不像屏幕那样1360*768这种,可能一个场景中的边界的距离就是30-60左右,所以计算的时候要注意误差问题。

 

使用GL:

GL是Unity里面的一个底层的图形库,其实是GL立即绘制函数 只用当前material的设置。因此除非你显示指定mat,否则mat可以是任何材质。并且GL可能会改变材质。

GL是立即执行的,如果你在Update()里调用,它们将在相机渲染前执行,相机渲染将会清空屏幕,GL效果将无法看到。通常GL用法是,在camera上贴脚本,并在OnPostRender()里执行。

注意:

1.GL的线等基本图元并没有uv. 所有是没有贴图纹理影射的,shader里仅仅做的是单色计算或者对之前的影像加以处理。所以GL的图形不怎么美观,如果需要炫丽效果不推荐

2.GL所使用的shader里必须有Cull off指令,否则显示会变成如下

 

使用GL的一般步骤是:

这是官方的一个例子,小作修改

public Vector3[] _GLPos; //我们要绘制的点
public IsDraw3D = true;  //3d或者2d绘制
static Material lineMaterial;

static void CreateLineMaterial()

{

      if (!lineMaterial)

        {

            lineMaterial = new Material("Shader "Lines/Colored Blended" {" +

                "SubShader { Pass { " +

                "    Blend SrcAlpha OneMinusSrcAlpha " +

                "    ZWrite Off Cull Off Fog { Mode Off } " +

                "    BindChannels {" +

                "      Bind "vertex", vertex Bind "color", color }" +

                "} } }");

            lineMaterial.hideFlags = HideFlags.HideAndDontSave;

            lineMaterial.shader.hideFlags = HideFlags.HideAndDontSave;

        }

    }

    void OnPostRender()

    {

        float axisZ = Camera.main.transform.position.z;

        CreateLineMaterial();  

        lineMaterial.SetPass(0);

        if (!IsDraw3D)

        {

             GL.LoadOrtho();

        }     

        GL.Begin(GL.LINES);

        GL.Color(new Color(1, 1, 1));

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

        {

            GL.Vertex(_GLPos[i]);

        }        

        GL.End();

    }

这其实是官方文档的一个例子,我进行了一些小小的修改,上面是部分摘录,脚本挂在摄像机中,在OnPostRender中的代码有些可能看起来很熟悉,没错,在window编程中我们绘制的时候也是类似于这种写法,底层使用的是状态机,设置各种绘制属性(java,windows,or mfc甚至是directx,设置画刷颜色,线框等),在下一次改变之前都是使用前一次设置的属性。其实使用的方法不难,但是这里有一个关键点:

GL.LoadOrtho();

这个方法的意思是使用设置ortho perspective,即水平视角。范围(0,0,-1) to (1,1,100). (还是坐标问题)主要用于在纯2D里绘制图元。GL.Vertex3()的取值范围从左下角的(0,0,0) 至右上角的(1,1,0),所以我们如果设置了这个东西,那么我们的代码要这么写:

假如我们要使用vecTemp这个世界坐标赋值给

坐标转换就不多说了。世界坐标转屏幕坐标,在缩放到0-1的范围

Vector3 posGL = Camera.main.WorldToScreenPoint(vecTemp);

posGL = new Vector3(posGL.x / Screen.width, posGL.y / Screen.height, 0);

_GLPos[i] = posGL;

 

我们要把坐标限制在范围(0,0,-1) to (1,1,100)内就需要进行相应的缩放,很明显,vecTemp是屏幕坐标了

如果没有调用 GL.LoadOrtho();那就是使用普通世界坐标了

_GLPos[i] = new Vector3(vecTemp.x, vecTemp.y, 0);

 

下面来看看,是否使用GL.LoadOrtho();的不同效果,可以看出感觉是一样的,但是坐标其实不一样,图1 数值x,y轴的范围始终在0-1之间,图2,会比1

图一

图二

使用Unity的内置几何体

在绘制点线的时候,如果可以的话,其实可以直接使用Unity的内置几何体,如Cube,Sphere这些直接拿来用,坐标使用的就是普通的世界坐标,比LineRenderer有时候更加简单方便。可以使用如Sphere制作一个自己满意的点,做成预制体,动态加载进场景中,数量我们根据自己的需求定。然后根据我们的具体情况设置世界坐标即可。

 

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引