• 【Unity Shader学习笔记】(五)使用鼠标绘制自由多边形(附完整工程源码)


    前言

    在前面的文章中,我们已经了解了怎样使用Unity Shader来绘制简单的点和线,本文将延续上次的话题,讲述一下如何在场景中使用Unity Shader绘制自由多边形。

    本文所述的程序,支持在地图中用鼠标点击,确定多边形顶点,并且绘制多边形的边,在内部填充半透明的颜色。先展示一下最终效果。完整工程下载地址在本文末尾处。


    1 开发工具介绍

    Windows 10(64位)

    Unity 5.4.1(64位)

    2 建立工程

    首先建立一个新工程,命名为Polygon,并创建一个Scene。在场景中新建一个Plane,该Plane是默认带有碰撞体的,这个碰撞体必须有,因为我们在后边使用鼠标选取位置的时候,涉及到碰撞检测。给该Plane加上贴图。


    3 核心代码实现

    3.1  Polygon.cs脚本中实现的是鼠标点击和向shader传递信息的功能

    (1)为了实现鼠标点选场景中的3D位置,需要使用射线

    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);  
    RaycastHit hit;  
    if (Physics.Raycast(ray, out hit, 100))  
    {  
    	Debug.DrawLine(ray.origin, hit.point);  					 
    }  

    (2)向shader传递顶点的位置和数量

    mat.SetVectorArray("Value",screenPos); //传递顶点屏幕位置信息给shader
    mat.SetInt ("PointNum",pointNum2Shader); //传递顶点数量给shader
    

    (3)将鼠标点击的位置转化为屏幕坐标

    worldPos[currentpointNum-1] = hit.point;
    Vector3 v3 = Camera.main.WorldToScreenPoint (worldPos [currentpointNum-1]);
    screenPos[currentpointNum-1] = new Vector4(v3.x,Screen.height-v3.y,v3.z,0);
    

    3.2 Polygon.shader中实现多边形的绘制功能

    (1)计算两点之间的距离函数

    float Dis(float4 v1,float4 v2)
    {
    	return sqrt(pow((v1.x-v2.x),2)+pow((v1.y-v2.y),2));
    }  

    (2)绘制线段的函数

    bool DrawLineSegment(float4 p1, float4 p2, float lineWidth,v2f i)
    {
        float4 center = float4((p1.x+p2.x)/2,(p1.y+p2.y)/2,0,0);
        //计算点到直线的距离  
         float d = abs((p2.y-p1.y)*i.vertex.x + (p1.x - p2.x)*i.vertex.y +p2.x*p1.y -p2.y*p1.x )/sqrt(pow(p2.y-p1.y,2) + pow(p1.x-p2.x,2));  
        //小于或者等于线宽的一半时,属于直线范围  
        float lineLength = sqrt(pow(p1.x-p2.x,2)+pow(p1.y-p2.y,2));
        if(d<=lineWidth/2 && Dis(i.vertex,center)<lineLength/2)  
        {  
            return true;  
        }  
        return false;
    }

    (3)绘制多边形的函数

    参考:https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html

    bool pnpoly(int nvert, float4 vert[6], float testx, float testy)
    {
        
        int i, j;
        bool c=false;
        float vertx[6];
        float verty[6];
    
        for(int n=0;n<nvert;n++)
        {
            vertx[n] = vert[n].x;
            verty[n] = vert[n].y;
        }
        for (i = 0, j = nvert-1; i < nvert; j = i++) {
        if ( ((verty[i]>testy) != (verty[j]>testy)) && (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
           c = !c;
        }
        return c;
    }

    4 完整的C#脚本和Shader代码

    Ploygon.cs

    using UnityEngine;
    using System.Collections;
    [ExecuteInEditMode] 
    public class Polygon : MonoBehaviour {
    
    	public Material mat; //绑定材质
    	Vector3[] worldPos; //存储获取的3D坐标
    	Vector4[] screenPos; //存储待绘制的多边形顶点屏幕坐标
    	int maxPointNum=6;  //多边形顶点总数
    	int currentpointNum =0; //当前已经获得的顶点数
    	int pointNum2Shader =0; //传递顶点数量给shader
    	bool InSelection=true; //是否处于顶点获取过程
    
    	void Start () {
    		worldPos = new Vector3[maxPointNum];
    		screenPos = new Vector4[maxPointNum];
    	}
    	
    
    	void Update () {
    		mat.SetVectorArray("Value",screenPos); //传递顶点屏幕位置信息给shader
    		mat.SetInt ("PointNum",pointNum2Shader); //传递顶点数量给shader
    
    		//使用摄像机发射一条射线,以获取要选择的3D位置
    		Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);  
    		RaycastHit hit;  
    		if (Physics.Raycast(ray, out hit, 100))  
    		{  
    			Debug.DrawLine(ray.origin, hit.point);  					 
    		}  
    
    		//利用鼠标点击来获取位置信息
    		if (Input.GetMouseButtonDown (0)&& InSelection) 
    		{
    			if (currentpointNum < maxPointNum) 
    			{
    				currentpointNum++;
    				pointNum2Shader++;
    				worldPos[currentpointNum-1] = hit.point;
    				Vector3 v3 = Camera.main.WorldToScreenPoint (worldPos [currentpointNum-1]);
    				screenPos[currentpointNum-1] = new Vector4(v3.x,Screen.height-v3.y,v3.z,0);
    			} 
    			else 
    			{
    				InSelection = false;
    			}
    		}
    
    		//实时更新已选择的3D点的屏幕位置
    		for (int i = 0; i < maxPointNum; i++) 
    		{
    			Vector3 v3 = Camera.main.WorldToScreenPoint (worldPos[i]);
    			screenPos[i] = new Vector4(v3.x,Screen.height-v3.y,v3.z,0);
    
    		}
    
    		//检测是否有3D点移动到了摄像机后面,如果有,则停止绘制
    		for (int i = 0; i < currentpointNum; i++) 
    		{
    			if (Vector3.Dot(worldPos[i]- Camera.main.transform.position,Camera.main.transform.forward)<=0) 
    			{
    				pointNum2Shader=0;
    				break;
    			}
    			pointNum2Shader= currentpointNum;
    		}
    	}
    
    	//抓取当前的渲染图像进行处理
    	void OnRenderImage(RenderTexture src, RenderTexture dest) {  
    		Graphics.Blit(src, dest, mat);  
    	}  		
    }


    Polygon.shader

    Shader "Unlit/polygon"
    {
    	Properties  
        {  
            //定义基本属性,可以从编辑器里面进行设置的变量  
            // _MainTex ("Texture", 2D) = "white" {}  
        }  
    
    	CGINCLUDE
    			//从应用程序传入顶点函数的数据结构定义 
    	 		struct appdata  
    	        {  
    	            float4 vertex : POSITION;  
    	            float2 uv : TEXCOORD0;  
    	        };  
    	        //从顶点函数传入片段函数的数据结构定义  
    	        struct v2f  
    	        {  
    	            float2 uv : TEXCOORD0;  
    	            float4 vertex : SV_POSITION;  
    	        };  
    	        //定义贴图变量  
    	        sampler2D _MainTex;  
    	        // float4 _MainTex_ST;  
    
    	        //定义与脚本进行通信的变量
    	        vector Value[6]; 
    	        int PointNum =0;
    
    	        //计算两点间的距离的函数
    	        float Dis(float4 v1,float4 v2)
    	        {
    	        	return sqrt(pow((v1.x-v2.x),2)+pow((v1.y-v2.y),2));
    	        }   
    
    	        //绘制线段
    	        bool DrawLineSegment(float4 p1, float4 p2, float lineWidth,v2f i)
    	        {
    	            float4 center = float4((p1.x+p2.x)/2,(p1.y+p2.y)/2,0,0);
    	            //计算点到直线的距离  
    	            float d = abs((p2.y-p1.y)*i.vertex.x + (p1.x - p2.x)*i.vertex.y +p2.x*p1.y -p2.y*p1.x )/sqrt(pow(p2.y-p1.y,2) + pow(p1.x-p2.x,2));  
    	            //小于或者等于线宽的一半时,属于直线范围  
    	            float lineLength = sqrt(pow(p1.x-p2.x,2)+pow(p1.y-p2.y,2));
    	            if(d<=lineWidth/2 && Dis(i.vertex,center)<lineLength/2)  
    	            {  
    	                return true;  
    	            }  
    	            return false;
    	        }
    
    	        //绘制多边形,这里限制了顶点数不超过6。可以自己根据需要更改。
    	        bool pnpoly(int nvert, float4 vert[6], float testx, float testy)
    	        {
    	            
    	            int i, j;
    	            bool c=false;
    	            float vertx[6];
    	            float verty[6];
    
    	            for(int n=0;n<nvert;n++)
    	            {
    	                vertx[n] = vert[n].x;
    	                verty[n] = vert[n].y;
    	            }
    	            for (i = 0, j = nvert-1; i < nvert; j = i++) {
    	            if ( ((verty[i]>testy) != (verty[j]>testy)) && (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
    	               c = !c;
    	            }
    	            return c;
    	        }
    	                        
    	        v2f vert (appdata v)  
    	        {  
    	            v2f o;  
    	            //将物体顶点从模型空间换到摄像机剪裁空间,也可采用简写方式——o.vertex = UnityObjectToClipPos(v.vertex);  
    	            o.vertex = mul(UNITY_MATRIX_MVP,v.vertex);  
    	            //2D UV坐标变换,也可以采用简写方式——o.uv = TRANSFORM_TEX(v.uv, _MainTex);  
    	            //o.uv = v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;  
    	            return o;  
    	        }             
    	        fixed4 frag (v2f i) : SV_Target  
    	        {  
    	            
    	       		//绘制多边形顶点
    	            for(int j=0;j<PointNum;j++)
    	            {
    	                if(Dis(i.vertex, Value[j])<3)
    	                {
    	                    return fixed4(1,0,0,0.5);
    	                }
    	            }
    	            //绘制多边形的边
    	            for(int k=0;k<PointNum;k++)
    	            {
    	                if(k==PointNum-1)
    	                {
    	                    if(DrawLineSegment(Value[k],Value[0],2,i))
    	                    {
    	                        return fixed4(1,1,0,0.5);
    	                    }
    	                }
    	                else
    	                {
    	                    if(DrawLineSegment(Value[k],Value[k+1],2,i))
    	                    {
    	                        return fixed4(1,1,0,0.5);
    	                    }
    	                }
    
    	            }
    	            //填充多边形内部
    	            if(pnpoly(PointNum, Value,i.vertex.x ,i.vertex.y))
    	            {
    	                return fixed4(0,1,0,0.3);
    	            }
    	            return fixed4(0,0,0,0);
    	            //fixed4 col = tex2D(_MainTex, i.uv); 
    	            //return col;  
    	        }  
    	ENDCG
    
        SubShader  
        {  
            Tags { "RenderType"="Opaque" }  
            LOD 100  
            Pass  
            {  
                //选取Alpha混合方式  
                Blend  SrcAlpha OneMinusSrcAlpha  
                //在CGPROGRAM代码块中写自己的处理过程  
                CGPROGRAM  
                //定义顶点函数和片段函数的入口分别为vert和frag  
                #pragma vertex vert  
                #pragma fragment frag  
                //包含基本的文件,里面有一些宏定义和基本函数  
                #include "UnityCG.cginc"               
               
                ENDCG  
            }  
        }  
    }

    5 运行效果





    小结

    本文介绍的是关于Unity Shader的一种基本应用。使用了简单的绘制技术,完成了在场景中进行自由多边形区域的选择功能。目前,还只是一种简单的实现,仅仅展示了绘制一个多边形。读者可以根据自己的需要,扩展其相关功能。


    ---------------------------------------------------------------------------------------------------------------------------------

    工程文件打包下载:

    链接: https://pan.baidu.com/s/1nvRSweH 密码: jr2g





  • 相关阅读:
    第1周作业
    第0次作业
    第三周作业
    随笔1
    第一次作业
    第二周作业
    第零次作业
    第四周作业
    第三周作业
    第二次作业
  • 原文地址:https://www.cnblogs.com/yanhuiqingkong/p/7770073.html
Copyright © 2020-2023  润新知