文章目录
👉一、前言及知识点
1、前言
开发时常遇到要动态修改物体或UI的颜色的需求,而且需要像Unity编辑器一样弹出颜色选择器来选择颜色。
心想如果还要从0开始开发一套取色器,那工作量可不少,最后我选择使用第三方颜色选择器插件ColorPicker来完成取色的功能。但在使用ColorPicker插件的过程中我发现用取色笔拾取电脑屏幕像素时会报错:attempting to ReadPixels outside of RenderTexture bounds! Reading (2396, 2905, 2412, 2921) from (1920, 1080)(意思是试图读取的像素超出纹理边界)
排查报错的原因:
原来ColorPicker插件的作者写取色笔拾色的逻辑是截取鼠标坐标点指定区域的图像,然后再通过获取图像中心点位置的像素点颜色来完成取色。由于插件源代码使用Texture2D.ReadPixels方法来绘制纹理图像,这个方法只能读取到当前分辨率的Game游戏窗口范围内像素,一旦超出这个范围就无法读取且会报错。
解决方法:Unity库中我没有找到可以读取Game窗口外电脑屏幕像素的方法,于是我将方向转到了.NET类库中。可以使用.net库中System.Drawing.dll截取电脑屏幕快照,创建图像的位图数据,将其转为字节数组,再将字节数组加载到Unity的纹理Texture图像上,最后读取该Texture纹理的像素颜色。
2、知识点
1. 用.Net类库System.Drawing截取鼠标指定区域图像
2. 将位图数据Bitmap转为字节数组
3. 将字节数据转为Unity的Texture纹理图像
4. 通过Unity的Texture.GetPixel()方法获取图片上指定点像素颜色
👉二、实现鼠标拾取电脑屏幕指定区域像素颜色
1、准备工作
导入.NET类库System.Drawing.dll。(使用搜索工具查找或者从你的unity安装路径下找到当前unity运行平台下的System.Drawing.dll)
2、使用.Net类库System.Drawing截取图像并转为Unity支持的纹理图像Texture
新建核心脚本GetScreenPixel.cs:
//引用依赖的命名空间
using UnityEngine;
using System.Drawing;
using System;
using System.Drawing.Imaging;
//获取屏幕像素点的类
public class GetScreenPixel
{
private static Bitmap bitmapSrc;//屏幕快照的位图数据
private static int multiple;//屏幕快照比例系数,可用于放大缩小
/// <summary>
/// 截取鼠标点的屏幕快照,将其转为Unity的Texture2D纹理图像
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Texture2D GetTexture(int width, int height)
{
Size size = new Size(width, height);//截取的大小
bitmapSrc = new Bitmap(width, height);//获取的位图大小
multiple = 1;
BitmapReset(bitmapSrc);//重置图片,解决超出屏幕部分图像残留BUG
System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmapSrc);//根据位图数据创建新图
g.CopyFromScreen(new Point(System.Windows.Forms.Cursor.Position.X - width / (2 * multiple), System.Windows.Forms.Cursor.Position.Y - height / (2 * multiple)), new Point(0, 0), size);//从屏幕上传输指定区域大小的图像数据到Graphics中绘制出来
IntPtr dc1 = g.GetHdc();
g.ReleaseHdc(dc1);//释放当前句柄
Texture2D tex = new Texture2D(width, height, TextureFormat.RGB24, false);
tex.LoadImage(BitmapToByte(bitmapSrc));//加载图像字节数组到纹理。
return tex;
}
/// <summary>
/// 将bitmap位图流转为字节流数组
/// </summary>
/// <param name="bitmap"></param>
/// <returns></returns>
public static byte[] BitmapToByte(System.Drawing.Bitmap bitmap)
{
// 1.先将BitMap转成内存流
System.IO.MemoryStream ms = new System.IO.MemoryStream();
//bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);//Unity加载时不支持bmp格式数据
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);//将位图数据保存为png类型的数据
ms.Seek(0, System.IO.SeekOrigin.Begin);
// 2.再将内存流转成byte[]并返回
byte[] bytes = new byte[ms.Length];
ms.Read(bytes, 0, bytes.Length);
ms.Dispose();
return bytes;
}
/// <summary>
/// 重置位图
/// </summary>
/// <param name="bitmap"></param>
private static void BitmapReset(Bitmap bitmap)
{
System.Drawing.Imaging.BitmapData bitmapdata = bitmap.LockBits(new Rectangle(new Point(0, 0), bitmap.Size), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
unsafe
{
byte* dataPointer = (byte*)(bitmapdata.Scan0.ToPointer());//数据矩阵在内存中的地址指针
for (int y = 0; y < bitmapdata.Height; y++)
{
for (int x = 0; x < bitmapdata.Width; x++)
{
dataPointer[0] = 0;
dataPointer[1] = 0;
dataPointer[2] = 0;
dataPointer[3] = 0;
dataPointer += 4;
}
}
}
bitmap.UnlockBits(bitmapdata);
}
}
3、需要注意的点
1.将位图数据转为字节流时需要指定图片格式为png或jgp类型(Unity支持的格式),否则会出现unity加载纹理时显示红色问号图像的情况。
2.在GetScreenPixel脚本中处理位图时用到了c++的指针,如果是使用Unity2018以后的版本,请在PlayerSetting/OtherSetting中勾选Allow ‘unsafe’ Code:否则编译器可能会报无法识别不安全代码的错误。
👉三、集成到ColorPicker插件源代码中完成取色笔的功能
ColorPicker插件的核心脚本就是ColorPicker,我们需要在该脚本中调用GetScreenPixel脚本的GetTexture()方法,以获取鼠标点屏幕快照,并读取得到的图像中心点的像素颜色。除此之外,还要注释掉一些代码。
1、修改ColorPicker脚本中的源代码
1.添加GetScreenColor方法,并在Update函数里调用该方法和注释掉原来截图的方法
/// 获取电脑屏幕鼠标点像素的方法
private void GetScreenColor()
{
var xCount = m_imageMesh.XAxisCount;//取色区域的宽
var yCount = m_imageMesh.YAxisCount;//取色区域的高
m_texture = GetScreenPixel.GetTexture(xCount, yCount);//获取鼠标点取色区域指定宽高的图像
m_screenImage.sprite = Sprite.Create(m_texture, new Rect(0, 0, xCount, yCount), Vector2.zero);//创建图像的精灵图赋值给取色的image
}
2.添加OnApplicationFocus(bool focus)方法
因为当鼠标点在Game窗口外是不响应鼠标点击函数的,所以选择屏幕外像素颜色时,我们要用到OnApplicationFocus函数,也就是运行unity时在后台中点击鼠标的话,unity程序会“失去焦点”,会调用一次OnApplicationFocus函数,传入的参数是False。此时我们就可以设置取色器上的颜色为当前颜色面板图的中心点的像素颜色了。
/// 当程序失去焦点时,点击程序Game画面外focus为false
public void OnApplicationFocus(bool focus)
{
if (!focus)
{
//获取图片上中心点位置像素颜色
Color = m_screenImage.sprite.texture.GetPixel(m_imageMesh.XAxisCount / 2 + 1, m_imageMesh.YAxisCount / 2 + 1);
SetNoniusPositionByColor();
WorkState = E_WorkState.Normal;
}
}
3.注释掉脚本上OnDisable函数里的内容
因为初始是要隐藏掉取色器面板的,隐藏时会调用该函数,将按钮事件监听都移除了,就无法响应鼠标事件了,所以需要注释或删掉。
2、使用修改后的ColorPicker插件取色笔功能修改Image和Text的颜色
1.搭建测试场景
分别新建image和text用来测试取色器修改后取色笔的功能。
2.写测试脚本
因为ColorPicker脚本为我们提供了取色器面板上的颜色访问器:
所以在需要修改的物体和UI的颜色脚本里,声明一下ColorPicker对象,调用该对象的颜色访问器即可。
如下脚本所示:
using UnityEngine;
using UnityEngine.UI;
public class SetColorTest : MonoBehaviour
{
public GameObject colorPanel;//取色器面板
public Image setColorImg;//需要修改颜色的image
public Text setColorText;//需要修改颜色的text
public Button closeBtn;
private Button pickBtn;
private SpringGUI.ColorPicker picker;//声明一个取色器对象
private bool bPickColor = false;//是否拾取颜色
private void Start()
{
pickBtn = setColorImg.GetComponent<Button>();
picker = colorPanel.transform.Find("ColorPicker").GetComponent<SpringGUI.ColorPicker>();
colorPanel.SetActive(false);
closeBtn.onClick.AddListener(() =>
{
colorPanel.SetActive(false);//关闭取色器
bPickColor = false;
});
pickBtn.onClick.AddListener(() =>
{
colorPanel.gameObject.SetActive(true);//打开取色器
bPickColor = true;
});
}
private void Update()
{
if (bPickColor && picker!=null && picker.gameObject.activeSelf)
{
//将测试的Image和Texty颜色更新为拾色器面板选择的颜色
setColorImg.color = picker.Color;
setColorText.color = picker.Color;
}
}
}
3、测试修改后的取色笔功能
1.编辑器内测试
2.打包exe后测试
Perfect!完美😄