做了一个VR的自由涂鸦画板,需要判断是否在指定位置涂鸦。
1.效果
2.思路
网上找了好多资料,最后缝合起来的。 (:з」∠)
自由涂鸦画板实现思路:
使用Texture.GetPixels32()
获取纹理的像素数组(Color32[]),将画笔与画板碰撞点的像素改为画笔的颜色,最后将修改后的像素数组用Texture.SetPixels32()
设置给纹理。
判断是否按规定轨迹涂鸦思路:
涂鸦过程实际是操作的一个像素数组(Color32[]),数组内储存着纹理每个像素的颜色。将指定的轨迹用N个单位为一像素的点标记,在绘制结束后判断这几个像素是否被修改成了画笔的颜色,如果轨迹内点的颜色都被修改,说明是按轨迹涂鸦的。
3.实现步骤
涂鸦画板:
新建一个Quad作为画板,复制一份Y轴降低一点作为画板背景。新建一个材质,渲染模式改为Transparent,PS里做一张背景为透明的图片作为纹理,别做太大会很卡,我做的是512*512,必须是方形的,不然涂鸦时位置对不上,可能是我缝合时坐标转换没处理好。导入Unity后将图片的Read/Write Enable打勾。设置画板Tag为Blackboard,挂载脚本。
public class Blackboard : MonoBehaviour
{
//画板尺寸
private int m_TextureWidth;
private int m_TextureHeight;
//当前画板图片
private Texture2D m_CurrentTexture;
//当前编辑的颜色数组
private Color32[] m_CurrentColors;
//重置画板的颜色数组
private Color[] m_CleanColorsArray;
//当前画板的绘制颜色
private Color m_CurrentBrushColor;
//用于检测笔位置的Plane
private Plane m_BoardPlane;
public Plane BoardPlane
{
get => m_BoardPlane;
}
//先前拖拽的位置
private Vector2 previous_drag_position;
//用于获取HDRP Lit Shader主帖图的字段
private static readonly int m_BaseColorMap = Shader.PropertyToID("_BaseColorMap");
private void Awake()
{
m_BoardPlane = new Plane(transform.forward, transform.position);
//获取画板尺寸
var l_originTexture = GetComponent<MeshRenderer>().material.mainTexture as Texture2D;
m_TextureWidth = l_originTexture.width;
m_TextureHeight = l_originTexture.height;
var l_originColor = l_originTexture.GetPixels32();
//新建一个纹理赋予材质
m_CurrentTexture = new Texture2D(l_originTexture.width, l_originTexture.height);
m_CurrentTexture.SetPixels32(l_originColor);
m_CurrentTexture.Apply();
//HDRP使用这种方式给材质设置纹理,普通项目使用注释掉的方法赋值
//GetComponent<MeshRenderer>().material.mainTexture = m_CurrentTexture;
GetComponent<MeshRenderer>().material.SetTexture(m_BaseColorMap, m_CurrentTexture);
}
public void DrawStop()
{
previous_drag_position = Vector2.zero;
}
public void UpdateDisplay(Vector3 _worldPoint, Vector2 _uvPoint, int _brushWidth, Color32 _brushColor)
{
Vector2 pixel_pos = UVToPixelCoordinates(_uvPoint);
m_CurrentColors = m_CurrentTexture.GetPixels32();
if (previous_drag_position == Vector2.zero)
{
// 如果这是我们第一次在该图像上拖动,只需在鼠标位置上为像素着色
MarkPixelsToColour(pixel_pos, _brushWidth, _brushColor);
}
else
{
// 在上次更新呼叫所在的行中显示颜色
ColourBetween(previous_drag_position, pixel_pos, _brushWidth, _brushColor);
}
ApplyMarkedPixelChanges();
previous_drag_position = pixel_pos;
}
/// <summary>
/// UV坐标转像素坐标
/// </summary>
private Vector2 UVToPixelCoordinates(Vector2 _vector2)
{
// 需要以我们的坐标为中心
float centered_x = _vector2.x * m_TextureWidth;
float centered_y = _vector2.y * m_TextureHeight;
// 将当前鼠标位置四舍五入到最近的像素
Vector2 pixel_pos = new Vector2(Mathf.RoundToInt(centered_x), Mathf.RoundToInt(centered_y));
return pixel_pos;
}
/// <summary>
/// 计算需要绘制的像素数量
/// </summary>
public void MarkPixelsToColour(Vector2 center_pixel, int pen_thickness, Color color_of_pen)
{
//找出每个方向(x和y)需要着色的像素数量
int center_x = (int) center_pixel.x;
int center_y = (int) center_pixel.y;
//int extra_radius = Mathf.Min(0, pen_thickness - 2);
for (int x = center_x - pen_thickness; x <= center_x + pen_thickness; x++)
{
// 检查X是否环绕图像,因此我们不在图像的另一侧绘制像素
if (x >= m_TextureWidth || x < 0)
continue;
for (int y = center_y - pen_thickness; y <= center_y + pen_thickness; y++)
{
MarkPixelToChange(x, y, color_of_pen);
}
}
}
/// <summary>
/// 俩点之间插入过渡点
/// </summary>
private void ColourBetween(Vector2 start_point, Vector2 end_point, int width, Color color)
{
// 获取从头到尾的距离
float distance = Vector2.Distance(start_point, end_point);
Vector2 direction = (start_point - end_point).normalized;
Vector2 cur_position = start_point;
// 根据自上次更新以来经过的时间,计算在start_point和end_point之间进行插值的次数
float lerp_steps = 1 / distance;
for (float lerp = 0; lerp <= 1; lerp += lerp_steps)
{
cur_position = Vector2.Lerp(start_point, end_point, lerp);
MarkPixelsToColour(cur_position, width, color);
}
}
/// <summary>
/// 修改像素数组信息
/// </summary>
public void MarkPixelToChange(int x, int y, Color color)
{
// 需要将x和y坐标转换为数组的平面坐标
int array_pos = y * m_TextureHeight + x;
// 检查这是一个有效的位置
if (array_pos > m_CurrentColors.Length || array_pos < 0)
return;
m_CurrentColors[array_pos] = color;
}
/// <summary>
/// 将新的像素数组赋值给纹理
/// </summary>
public void ApplyMarkedPixelChanges()
{
m_CurrentTexture.SetPixels32(m_CurrentColors);
m_CurrentTexture.Apply();
}
/// <summary>
/// 笔尖在画板正面还是背面
/// </summary>
/// <param name="_point">笔尖的位置</param>
/// <returns>>当在正面的时候返回正值,当在背面的时候返回负值</returns>
public bool GetSideOfBoardPlane(Vector3 _point)
{
return m_BoardPlane.GetSide(_point);
}
/// <summary>
/// 笔尖距画板的距离
/// </summary>
/// <param name="_point"></param>
/// <returns></returns>
public float GetDistanceFromBoardPlane(Vector3 _point)
{
return m_BoardPlane.GetDistanceToPoint(_point);
}
/// <summary>
/// 矫正后的笔尖应该在的位置
/// </summary>
/// <param name="point">笔尖的位置</param>
/// <returns>矫正后的笔尖位置</returns>
public Vector3 ProjectPointOnBoardPlane(Vector3 point)
{
float d = -Vector3.Dot(m_BoardPlane.normal, point - transform.position);
return point + m_BoardPlane.normal * d;
}
}
用一些基础模型组装一支笔。
TransformModify是用来调整握持位置的,需要赋值给抓取脚本的Snap Handle,RayOrigin是射线起点,BrushHead是一个空物体放在笔的最前端。
VRTK_SDKTransformModify的使用:
在需要调整握持位置物体下创建空物体挂载VRTK_SDKTransformModify脚本;
将GameObject拖拽给Target,SdkOverrides添加要修正握持位置的平台,我的是HTC Vive;
将创建的空物体赋值给握持物体GrabAttach脚本(有不同的握持类型,我使用的是ChildOfControllerGrabAttach)的snap handle;
在编辑器运行状态下调整握持物体的位置,调整完毕后记录Transform信息填入对应的平台中。
笔调整好之后挂载脚本:
public class PenBrush : MonoBehaviour
{
[SerializeField] private Color32 m_BrushColor;
[SerializeField] private int m_BrushWidth;
[SerializeField] private Transform m_RayOrigin;
[SerializeField] private float m_RayLength;
[SerializeField] private Blackboard m_Blackboard;
private bool m_IsGrab;
private RaycastHit m_HitInfo;
private void Awake()
{
//扩展的物体抓取脚本
GetComponent<BrushGrabAttach>().OnStartGrad += OnStartGrab;
GetComponent<BrushGrabAttach>().OnStopGrad += OnStopGrad;
}
private void OnStartGrab()
{
m_IsGrab = true;
}
private void OnStopGrad()
{
m_IsGrab = false;
m_Blackboard.DrawStop();
}
private bool m_PreviousHaveHit;
private void Update()
{
if (!m_IsGrab) return;
var l_ray = new Ray(m_RayOrigin.position, m_RayOrigin.forward);
Vector3 forward = m_RayOrigin.transform.TransformDirection(Vector3.forward) * m_RayLength;
//绘制射线,调试用的。需要打开Gizmos开关,不然不显示。
Debug.DrawRay(m_RayOrigin.position, forward, Color.magenta);
if (Physics.Raycast(l_ray, out m_HitInfo, m_RayLength))
{
if (m_HitInfo.collider.CompareTag("Blackboard"))
{
m_Blackboard.UpdateDisplay(m_HitInfo.point,m_HitInfo.textureCoord,m_BrushWidth,m_BrushColor);
m_PreviousHaveHit = true;
}
}
else if(m_PreviousHaveHit)
{
m_Blackboard.DrawStop();
m_PreviousHaveHit = false;
}
}
private void OnDestroy()
{
GetComponent<BrushGrabAttach>().OnStartGrad -= OnStartGrab;
GetComponent<BrushGrabAttach>().OnStopGrad -= OnStopGrad;
}
}
为例避免出现笔穿透画板的情况,扩展一下VRTK的抓取脚本:
public class BrushGrabAttach : VRTK_BaseGrabAttach
{
[Header("Painter Options")]
[SerializeField]
private Transform tips;//笔尖
[SerializeField] protected Blackboard board;//画板
public event UnityAction OnStartGrad;
public event UnityAction OnStopGrad;
#region 重写的父类方法
protected override void Initialise()
{
//初始化父类的一些字段,这些字段只是标识这个抓附机制的作用
tracked = false;
kinematic = false;
climbable = false;
//初始化自定义的属性
if (precisionGrab)//最好不要用精确抓取,因为这样很有可能会让笔处于一个不合理的位置,这样使用的时候,会很变扭(比如必须手腕旋转一个角度,笔才是正的)
{
Debug.LogError("PrecisionGrab cant't be true in case of PainterGrabAttach Mechanic");
}
}
public override bool StartGrab(GameObject grabbingObject, GameObject givenGrabbedObject, Rigidbody givenControllerAttachPoint)
{
if (base.StartGrab(grabbingObject, givenGrabbedObject, givenControllerAttachPoint))
{
SnapObjectToGrabToController(givenGrabbedObject);
grabbedObjectScript.isKinematic = true;
OnStartGrad?.Invoke();
return true;
}
return false;
}
public override void StopGrab(bool applyGrabbingObjectVelocity)
{
ReleaseObject(applyGrabbingObjectVelocity);
OnStopGrad?.Invoke();
base.StopGrab(applyGrabbingObjectVelocity);
}
public override void ProcessFixedUpdate()
{
if (grabbedObject)//只有抓住物体后,grabbedObject才不会
{
grabbedObject.transform.rotation = controllerAttachPoint.transform.rotation * Quaternion.Euler(grabbedSnapHandle.transform.localEulerAngles);
grabbedObject.transform.position = controllerAttachPoint.transform.position - (grabbedSnapHandle.transform.position - grabbedObject.transform.position);
float distance = board.GetDistanceFromBoardPlane(tips.position);//笔尖距离平面的距离
bool isPositiveOfBoardPlane = board.GetSideOfBoardPlane(tips.position);//笔尖是不是在笔尖的正面
Vector3 direction = grabbedObject.transform.position - tips.position;//笔尖位置指向笔的位置的差向量
//当笔尖穿透的时候,需要矫正笔的位置
if (isPositiveOfBoardPlane || distance > 0.0001f)
{
Vector3 pos = board.ProjectPointOnBoardPlane(tips.position);
grabbedObject.transform.position = pos - board.BoardPlane.normal * 0.001f + direction;//pos是笔尖的位置,而不是笔的位置,加上direction后才是笔的位置
}
}
}
#endregion
//让手柄抓住物体
private void SnapObjectToGrabToController(GameObject obj)
{
if (!precisionGrab)
{
SetSnappedObjectPosition(obj);
}
}
//设置物体和手柄连接的位置
private void SetSnappedObjectPosition(GameObject obj)
{
if (grabbedSnapHandle == null)
{
obj.transform.position = controllerAttachPoint.transform.position;
}
else
{
//设置旋转,controllerAttachPoint是手柄上的一个与物体的连接点
obj.transform.rotation = controllerAttachPoint.transform.rotation * Quaternion.Euler(grabbedSnapHandle.transform.localEulerAngles);
//因为grabbedSnapHandle和obj.transform之间可能不是同一个点,所以为了让手柄抓的位置是grabbedSnapHandle,需要减去括号中代表的向量
obj.transform.position = controllerAttachPoint.transform.position - (grabbedSnapHandle.transform.position - obj.transform.position);
}
}
}
现在画板和画笔挂载的脚本:
效果:
到此,普通的涂鸦功能已经实现,接下来加入轨迹识别。
轨迹识别:
在PS里做一张用于识别轨迹的图片。就是将轨迹用N个1像素的点绘制出来。
为了避免颜色干扰和减少计算量,新建一个透明图层,在这个图层上做标记。
绘图层就是有很多1像素点的图片。
绘制轨迹标记点时最好用铅笔工具,1像素100%硬度。每条轨迹的标记颜色色差最好大一点,太接近容易搞混,这里每条轨迹标记颜色需要记一下颜色数据,代码里是用颜色识别每一条轨迹的。将背景层和绘图层分别导出存为PNG格式。
图片导入Unity,背景层图片拖给背景的材质,绘图层托给画板的材质,记得将绘图层图片的Read/Write Enable打勾。
因为轨迹识别是分区域的,只有在A字母区域绘图时才识别A区域的轨迹是否被覆盖,所以要新建一个类来描述这个区域。需要在面板上编辑所以加上[Serializable]。
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class IdentifyAreas
{
public Transform upperLeft;
public Transform lowerRight;
//区域内的轨迹点颜色
public Color32 trackColor;
//提示完成的图片
public GameObject show;
//轨迹在绘图像素数组内的下标
public List<int> track = new List<int>();
/// <summary>
/// 检测传入的点是否在区域内
/// </summary>
public bool CheckPointInArea(Vector3 _point)
{
var l_pointX = _point.x;
var l_pointZ = _point.z;
var l_x = l_pointX > upperLeft.position.x && l_pointX < lowerRight.position.x;
var l_y = l_pointZ > lowerRight.position.z && l_pointZ < upperLeft.position.z;
return l_x && l_y;
}
/// <summary>
/// 颜色比对
/// </summary>
public bool CheckColor(Color32 _color32)
{
var l_resultR = Mathf.Abs(trackColor.r - _color32.r) <= 5;
var l_resultG = Mathf.Abs(trackColor.g - _color32.g) <= 5;
var l_resultB = Mathf.Abs(trackColor.b - _color32.b) <= 5;
return l_resultR && l_resultG && l_resultB;
}
}
使用俩个点的坐标来确定一个区域。
现在需要修改一下Blackboard脚本,添加区域数组,在Awake阶段读取绘图层的标记点,根据点的颜色将标记点的位置信息加到IdentifyAreas脚本的下标List中。在UpdateDisplay加入绘图区域的检测。绘制结束后添加判断区域内的轨迹点是否被覆盖的逻辑。
修改后的Blackboard:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
public class Blackboard : MonoBehaviour
{
//画板尺寸
private int m_TextureWidth;
private int m_TextureHeight;
//当前画板图片
private Texture2D m_CurrentTexture;
//当前编辑的颜色数组
private Color32[] m_CurrentColors;
//重置画板的颜色数组
private Color[] m_CleanColorsArray;
//当前画板的绘制颜色
private Color m_CurrentBrushColor;
//用于检测笔位置的Plane
private Plane m_BoardPlane;
public Plane BoardPlane
{
get => m_BoardPlane;
}
//先前拖拽的位置
private Vector2 previous_drag_position;
//用于获取HDRP Lit Shader主帖图的字段
private static readonly int m_BaseColorMap = Shader.PropertyToID("_BaseColorMap");
//目前绘制的区域(新加)
private IdentifyAreas m_CurrentArea;
//所有识别区(新加)
public IdentifyAreas[] areas;
private void Awake()
{
m_BoardPlane = new Plane(transform.forward, transform.position);
//获取画板尺寸
var l_originTexture = GetComponent<MeshRenderer>().material.mainTexture as Texture2D;
m_TextureWidth = l_originTexture.width;
m_TextureHeight = l_originTexture.height;
var l_originColor = l_originTexture.GetPixels32();
//新建一个纹理赋予材质
m_CurrentTexture = new Texture2D(l_originTexture.width, l_originTexture.height);
m_CurrentTexture.SetPixels32(l_originColor);
m_CurrentTexture.Apply();
GetComponent<MeshRenderer>().material.SetTexture(m_BaseColorMap, m_CurrentTexture);
//读取所有的标记点(新加)
for (int i = 0; i < l_originColor.Length; i++)
{
//过滤掉透明像素
if (l_originColor[i].a == 0) continue;
foreach (var l_area in areas)
{
//从Unity读取出来的颜色有色差(不会太大),不能直接比较,在IdentifyAreas新建了一个模糊比对方法。
if (l_area.CheckColor(l_originColor[i]))
l_area.track.Add(i);
}
}
}
public void DrawStop()
{
previous_drag_position = Vector2.zero;
//(新加)
if (m_CurrentArea != null)
CheckTrack();
}
//检查轨迹覆盖(新加)
private void CheckTrack()
{
foreach (var l_track in m_CurrentArea.track)
{
if (m_CurrentColors[l_track] != m_CurrentBrushColor)
return;
}
m_CurrentArea.show.SetActive(true);
Debug.Log("完成");
}
public void UpdateDisplay(Vector3 _worldPoint, Vector2 _uvPoint, int _brushWidth, Color32 _brushColor)
{
Vector2 pixel_pos = UVToPixelCoordinates(_uvPoint);
m_CurrentColors = m_CurrentTexture.GetPixels32();
if (previous_drag_position == Vector2.zero)
{
//(新加)
m_CurrentBrushColor = _brushColor;
//检测在哪个区域开始
foreach (var l_area in areas)
{
if (l_area.CheckPointInArea(_worldPoint))
m_CurrentArea = l_area;
}
// 如果这是我们第一次在该图像上拖动,只需在鼠标位置上为像素着色
MarkPixelsToColour(pixel_pos, _brushWidth, _brushColor);
}
else
{
// 在上次更新呼叫所在的行中显示颜色
ColourBetween(previous_drag_position, pixel_pos, _brushWidth, _brushColor);
}
ApplyMarkedPixelChanges();
previous_drag_position = pixel_pos;
}
/// <summary>
/// UV坐标转像素坐标
/// </summary>
private Vector2 UVToPixelCoordinates(Vector2 _vector2)
{
// 需要以我们的坐标为中心
float centered_x = _vector2.x * m_TextureWidth;
float centered_y = _vector2.y * m_TextureHeight;
// 将当前鼠标位置四舍五入到最近的像素
Vector2 pixel_pos = new Vector2(Mathf.RoundToInt(centered_x), Mathf.RoundToInt(centered_y));
return pixel_pos;
}
/// <summary>
/// 计算需要绘制的像素数量
/// </summary>
public void MarkPixelsToColour(Vector2 center_pixel, int pen_thickness, Color color_of_pen)
{
//找出每个方向(x和y)需要着色的像素数量
int center_x = (int) center_pixel.x;
int center_y = (int) center_pixel.y;
//int extra_radius = Mathf.Min(0, pen_thickness - 2);
for (int x = center_x - pen_thickness; x <= center_x + pen_thickness; x++)
{
// 检查X是否环绕图像,因此我们不在图像的另一侧绘制像素
if (x >= m_TextureWidth || x < 0)
continue;
for (int y = center_y - pen_thickness; y <= center_y + pen_thickness; y++)
{
MarkPixelToChange(x, y, color_of_pen);
}
}
}
/// <summary>
/// 俩点之间插入过渡点
/// </summary>
private void ColourBetween(Vector2 start_point, Vector2 end_point, int width, Color color)
{
// 获取从头到尾的距离
float distance = Vector2.Distance(start_point, end_point);
Vector2 direction = (start_point - end_point).normalized;
Vector2 cur_position = start_point;
// 根据自上次更新以来经过的时间,计算在start_point和end_point之间进行插值的次数
float lerp_steps = 1 / distance;
for (float lerp = 0; lerp <= 1; lerp += lerp_steps)
{
cur_position = Vector2.Lerp(start_point, end_point, lerp);
MarkPixelsToColour(cur_position, width, color);
}
}
/// <summary>
/// 修改像素数组信息
/// </summary>
public void MarkPixelToChange(int x, int y, Color color)
{
// 需要将x和y坐标转换为数组的平面坐标
int array_pos = y * m_TextureHeight + x;
// 检查这是一个有效的位置
if (array_pos > m_CurrentColors.Length || array_pos < 0)
return;
m_CurrentColors[array_pos] = color;
}
/// <summary>
/// 将新的像素数组赋值给纹理
/// </summary>
public void ApplyMarkedPixelChanges()
{
m_CurrentTexture.SetPixels32(m_CurrentColors);
m_CurrentTexture.Apply();
}
/// <summary>
/// 笔尖在画板正面还是背面
/// </summary>
/// <param name="_point">笔尖的位置</param>
/// <returns>>当在正面的时候返回正值,当在背面的时候返回负值</returns>
public bool GetSideOfBoardPlane(Vector3 _point)
{
return m_BoardPlane.GetSide(_point);
}
/// <summary>
/// 笔尖距画板的距离
/// </summary>
/// <param name="_point"></param>
/// <returns></returns>
public float GetDistanceFromBoardPlane(Vector3 _point)
{
return m_BoardPlane.GetDistanceToPoint(_point);
}
/// <summary>
/// 矫正后的笔尖应该在的位置
/// </summary>
/// <param name="point">笔尖的位置</param>
/// <returns>矫正后的笔尖位置</returns>
public Vector3 ProjectPointOnBoardPlane(Vector3 point)
{
float d = -Vector3.Dot(m_BoardPlane.normal, point - transform.position);
return point + m_BoardPlane.normal * d;
}
}
接下来到面板上编辑IdentifyAreas信息:
到此,文章开头的效果已经实现。
4.其他功能
板檫:
板擦和画笔的功能一样,板擦的颜色是画板的原始色。
画笔和板擦的抓取方式不同,重写一下之前抓取脚本里的方法。
using UnityEngine;
public class EraserGrabAttach : BrushGrabAttach
{
public override void ProcessFixedUpdate()
{
if (grabbedObject)//只有抓住物体后,grabbedObject才不会
{
grabbedObject.transform.rotation = controllerAttachPoint.transform.rotation * Quaternion.Euler(grabbedSnapHandle.transform.localEulerAngles);
grabbedObject.transform.position = controllerAttachPoint.transform.position - (grabbedSnapHandle.transform.position - grabbedObject.transform.position);
float distance = board.GetDistanceFromBoardPlane(transform.position);//黑板檫距离画板的距离
if (distance > -0.096f)//当黑板檫离画板足够近的时候
{
float percentOfDistance = Helper.RemapNumberClamped(distance, -0.096f, -0.05f, 0f, 1f);//映射后,得到插值系数
Quaternion q = Quaternion.FromToRotation(grabbedObject.transform.up, -board.transform.forward);//最终的目的是:黑板擦的transform.up指向-transform.forward
q *= grabbedObject.transform.rotation;//得到黑板檫达到最终目的时的旋转
grabbedObject.transform.rotation = Quaternion.Slerp(grabbedObject.transform.rotation, q, percentOfDistance);//通过插值,得到当前黑板檫的旋转
if (distance > 0.01f)//如果黑板檫穿透了画板,需要进行矫正
{
Vector3 pos = board.ProjectPointOnBoardPlane(grabbedObject.transform.position);
grabbedObject.transform.position = pos - board.transform.forward * 0.01f;
}
}
}
}
}
5.存在的问题
判断的是标记点的颜色是否被修改成画笔颜色,要是将区域内全部涂鸦也会触发完成逻辑。暂时想到的解决办法是在区域内用其他颜色标记几个辅助点,做判断时这几个点的颜色不能被修改。