当前位置: 首页 > news >正文

Unity引擎修改模型顶点色的工具

大家好,我是阿赵。
  之前分享过怎样通过MaxScript在3DsMax里面修改模型的顶点色。不过由于很多时候顶点色的编辑需要根据在游戏引擎里面的实际情况和shader的情况来动态调整,所以如果能在引擎里面直接修改模型的顶点色,将会方便很多。于是我写了下面这个在Unity引擎里面修改模型顶点色的工具。

一、功能介绍

1、模型选择和网格生成

这是工具的界面,选择一个或者多个带有网格模型的GameObject,点击开始编辑
在这里插入图片描述

如果选择的网格模型是属于不能编辑的类型,比如unity自带网格,fbx里面的网格,工具会提示生成可编辑网格
在这里插入图片描述

工具将会把网格模型复制一份,保存成asset格式的资源文件。

2、笔刷功能

开始编辑之后,把鼠标放在模型上,会出现笔刷,笔刷的大小、衰减、强度等可以调节
在这里插入图片描述
在这里插入图片描述

其中笔刷的半径可以用快捷键”[“和”]”控制,衰减可以用快捷键”-”和”=”来控制

3、顶点色显示

为了能在使用工具的过程中知道绘制的效果,所以有必要把顶点色给显示出来。
在这里插入图片描述

不过如果模型默认有颜色,可能会导致刷的过程比较难看得清楚,所以也可以进入线框模式看
在这里插入图片描述
在这里插入图片描述

如果模型的顶点很多,那么显示顶点也会比较耗时,表现就是在刷颜色的时候有点卡,所以也提供了一个显示模式,是指显示笔刷覆盖范围的点
在这里插入图片描述

为了便于观察单个通道的当前值,所以在单个通道模式下,可以选择单色显示或者是黑白显示,这样看起来会更加直观
在这里插入图片描述
在这里插入图片描述

4、刷颜色

  调一下强度,其实就是笔刷的透明度,就可以在模型上面刷顶点颜色了。如果是刷所有通道,可以直接选择颜色。如果是单个通道,可以选择是增加或者减少该通道的颜色。
在这里插入图片描述

  这里加入了权重模式的选项。所谓的权重模式,是保证RGBA4个通道的颜色值加起来等于1,如果增加某个通道的颜色,就会相应的减少其他几个通道的颜色。在用顶点色做某些混合功能的时候,这个权重模式会比较的有用。

二、技术点

1、判断笔刷位置

  本来很简单的一件事情,鼠标在屏幕位置发射射线,求和模型网格的碰撞点就可以了。不过如果用Physics.Raycast方法,是需要模型网格上有碰撞体才能检测到的。而在这个工具的环境下,我需要的是没有碰撞体也能检测得到网格模型的碰撞点。
  其实这个方法是Unity自带了的:

bool HandleUtility.IntersectRayMesh(Ray ray,Mesh mesh,Matrix4x4 matrix,out RaycastHit hit)

可惜的是,这个方法Unity居然是隐藏的,并没有暴露出来给我们用。所以只能使用反射的手段去调用:

private bool IntersectRayMesh(Ray ray,Mesh sharedMesh,Matrix4x4 matrix,out RaycastHit hit)
{System.Type type = typeof(HandleUtility);System.Reflection.MethodInfo method = type.GetMethod("IntersectRayMesh", (BindingFlags.Static | BindingFlags.NonPublic));RaycastHit tempHit = new RaycastHit();object[] objArr = new object[4];objArr[0] = ray;objArr[1] = sharedMesh;objArr[2] = matrix;objArr[3] = null;bool isSuccess = (bool)method.Invoke(null,objArr);hit = (RaycastHit)objArr[3];      return isSuccess;
}

2、锁定操作

在刷顶点色的时候,我们肯定不希望鼠标还有原来的操作行为,比如框选、位移旋转之类的操作,所以需要对操作进行锁定:
通过这个方法来锁定鼠标操作:
HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));
然后记录一下当前使用的工具,并把当前工具置为None。这样就不会出现移动的坐标轴之类了,记录当前使用的工具是为了退出编辑的时候可以还原。

LastTool = Tools.current;
Tools.current = Tool.None;

3、绘制笔刷和顶点

在Scene视图里面绘制图形,需要添加duringSceneGui 处理

SceneView.duringSceneGui += OnSceneGUI;
void OnSceneGUI(SceneView sceneView)
{
}

然后绘制的命令都写在OnSceneGUI里面。
这里主要用到了3个命令:
绘制线条

Handles.DrawLine

绘制圆盘

Handles.DrawWireDisc

绘制点

Handles.DotHandleCap

然后改变绘制颜色是用

Handles.color

4、在不影响mesh本身的情况下修改颜色

如果只是为了改变mesh的顶点色,那么直接修改Mesh.colors就可以了,不过这个毕竟只是一个编辑的过程,我们也有可能撤销修改,甚至直接不保存这次的修改。所以不能直接修改Mesh.colors。
我这里是使用了一个数据类MeshEditData来记录了每个mesh上面的所有顶点颜色,然后在刷顶点色的过程中,只是改变这个MeshEditData对象里面的颜色。等确定保存的时候,才把color数据写入mesh。

5、撤回操作

Unity本身是自带了Undo的操作的,按道理只需要调用Undo相关的方法,就可以实现撤回操作了。
不过不论是Undo.RecordObject还是Undo.RecordObjects,都需要传入Object作为参数,也就是Unity的对象。而我记录的是自定义对象,并不是Object,所以不能直接这么用。
于是可以这样操作:
通过注册undoRedoPerformed 方法,来自定义撤回的时候执行的方法
Undo.undoRedoPerformed += this.OnUndo;
在每次笔刷开始刷之前,也就是鼠标左键点下的时候,记录一下当前所有网格的颜色值,并存到一个数组里面。
然后在OnUndo方法里面,检查回撤的数组,如果有,就把数组最后入栈的颜色值取出,并赋值给当前的数据。
值得注意的有2点:
1.undoRedoPerformed 方法不论是ctrl+z还是ctrl+y都会执行的
2.undoRedoPerformed 方法是需要前面有操作,才会记录堆栈,如果我们一直在Scene视图刷颜色,他只会记录一次操作,所以也只能回退一次,这里需要每次绘制完一笔之后,也就是鼠标左键抬起的时候,手动调用一下Undo.RegisterCompleteObjectUndo方法

三、工具源码

总共3个c#脚本

using System.Collections;
using System.Collections.Generic;using UnityEngine;
using UnityEditor;
using System.IO;
using System.Reflection;namespace azhao.tools.VertexColorPainer
{public enum BrushChannelType{ALL = 0,RED = 1,GREEN = 2,BLUE = 3,ALPHA = 4}public enum SetColorMode{ADD = 0,WEIGHT = 1}public enum OperatorType{ADD = 0,REDUCE = 1}public class VertexColorPainerWin : EditorWindow{static private VertexColorPainerWin _instance;public static VertexColorPainerWin Instance{get{if (_instance == null){_instance = (VertexColorPainerWin)EditorWindow.GetWindow(typeof(VertexColorPainerWin));_instance.titleContent = new GUIContent("顶点颜色刷");_instance.maxSize = _instance.minSize = new Vector2(600, 600);}return _instance;}}[MenuItem("Tools/顶点颜色刷")]static void ShowWin(){VertexColorPainerWin.Instance.Show();}#region 生命周期// Start is called before the first frame updatevoid Start(){}private void OnEnable(){SceneView.duringSceneGui += OnSceneGUI;Undo.undoRedoPerformed -= this.OnUndo;Undo.undoRedoPerformed += this.OnUndo;}private void OnDisable(){SceneView.duringSceneGui -= OnSceneGUI;Undo.undoRedoPerformed -= this.OnUndo;Tools.current = LastTool;}void OnDestroy(){SceneView.duringSceneGui -= this.OnSceneGUI;Undo.undoRedoPerformed -= this.OnUndo;}// Update is called once per framevoid Update(){}#endregionTool LastTool = Tool.None;private bool isEditMode = false;private List<GameObject> meshObjList;List<MeshEditData> meshEditList;private string val = "";private float brushSize = 0.5f;private float brushFalloff = 1;private Color brushColor = Color.white;private bool isShowPointColor = false;private string[] channelSelectStr = new string[] { "所有通道", "R通道", "G通道", "B通道", "A通道" };private int channelSelectIndex = 0;private string[] showPointStr = new string[] { "不显示", "笔刷范围显示", "全部显示" };private int showPointIndex = 0;private string[] showColorType = new string[] { "原色", "单色","黑白" };private int showColorIndex = 0;string[] drawTypeStr = new string[] { "颜色叠加", "权重模式" };int drawTypeIndex = 0;string[] drawAddTypeStr = new string[] { "增加", "减少" };int drawAddTypeIndex = 0;private float brushSizeScale = 0.01f;private float brushAlpha = 1;private bool isPaint = false;void OnGUI(){ShowHelp();ShowSelectobj();if(isEditMode){if(HasSelectObj() == false){CleanMeshList();isEditMode = false;}ShowObjInfo();ShowTitle();ShowCtrl();ShowContent();}}#region 说明private bool isShowHelp = true;private void ShowHelp(){if(GUILayout.Button("来自阿赵的使用说明",GUILayout.Height(40))){isShowHelp = !isShowHelp;}if(isShowHelp == false){return;}GUILayout.Label("1、在场景中想编辑的模型,点击开始编辑");GUILayout.Label("2、如果模型中的网格不可编辑,会生成课编辑网格,路径在Assets/meshes/");GUILayout.Label("3、根据情况选择刷所有通道颜色还是单个通道颜色");GUILayout.Label("4、可以选择叠加模式或者权重模式");GUILayout.Label("\t叠加模式是直接绘制指定颜色");GUILayout.Label("\t权重模式是保证RGBA通道加起来等于1");GUILayout.Label("5、绘制不合适可以按Ctrl+Z撤回");}#endregion#region 物体选择编辑private bool HasSelectObj(){if(meshEditList == null||meshEditList.Count==0){return false;}if(meshObjList == null||meshObjList.Count==0){return false;}for(int i = 0;i<meshObjList.Count;i++){if(meshObjList[i]==null){return false;}}return true;}private void ShowSelectobj(){ShowLine("编辑模型顶点色工具");if (isEditMode == true){if (GUILayout.Button("保存编辑", GUILayout.Width(600), GUILayout.Height(40))){SaveMeshes(); }if (GUILayout.Button("退出编辑",GUILayout.Width(600),GUILayout.Height(40))){OnEditEnd();}}else{if (GUILayout.Button("开始编辑", GUILayout.Width(600), GUILayout.Height(40))){OnEditBegin();}}}private void OnEditEnd(){Tools.current = LastTool;if (HasEditableMesh()){if(ShowSelectTips("是否保存网格?", "保存", "退出")){SaveMeshes();}isEditMode = false;}}private bool HasEditableMesh(){if(meshEditList != null&& meshEditList.Count>0){return true;}return false;}private void OnEditBegin(){LastTool = Tools.current;Tools.current = Tool.None;meshObjList = new List<GameObject>();meshEditList = new List<MeshEditData>();GameObject[] gos = Selection.gameObjects;if(gos!=null){for(int i = 0;i<gos.Length;i++){Transform[] trs = gos[i].GetComponentsInChildren<Transform>();for(int j = 0;j<trs.Length;j++){if (trs[j].gameObject.activeSelf == true){if (meshObjList.IndexOf(trs[j].gameObject) < 0){meshObjList.Add(trs[j].gameObject);}}}}}if(meshObjList == null||meshObjList.Count==0){ShowTips("没有可以编辑的模型,请用鼠标选中场景中的模型");return;}if(CheckMeshCanEdit()==true){isEditMode = true;}}private void CleanMeshList(){meshEditList = null;}private bool CheckMeshCanEdit(int count = 0){count++;if(count>=3){return false;}if(meshObjList == null){return false;}meshEditList = new List<MeshEditData>();for(int i = 0;i<meshObjList.Count;i++){if(meshObjList[i].activeSelf == false){continue;}MeshEditData data = new MeshEditData(meshObjList[i]);if(data.mesh!=null){meshEditList.Add(data);}}if(HasEditableMesh()==false){
;CleanMeshList();ShowTips("选择的物体里面没有可以编辑的网格模型");return false;}bool hasOrigMesh = false;if(meshEditList != null&& meshEditList.Count>0){for(int i = 0;i< meshEditList.Count;i++){Mesh mesh = meshEditList[i].mesh;string path = AssetDatabase.GetAssetPath(mesh);if(path.EndsWith(".asset")==false){hasOrigMesh = true;break;}}}if(hasOrigMesh == true){                if (ShowSelectTips("当前模型里面有不可以直接编辑的网格,是否生成可编辑的网格?", "生成", "取消编辑")){bool successCreate = CreateCopyMeshAsset();if(successCreate == false){CleanMeshList();return false;}else{CleanMeshList();return CheckMeshCanEdit(count);}}else{CleanMeshList();return false;}}else{return true;}}private void SaveMeshes(){if (meshEditList != null && meshEditList.Count > 0){for (int i = 0; i < meshEditList.Count; i++){meshEditList[i].SaveMesh();}}AssetDatabase.SaveAssets();AssetDatabase.Refresh();}#endregion#region 笔刷操作编辑private void ShowObjInfo(){GUILayout.Label("当前编辑的对象:", GUILayout.Width(120));if(meshEditList != null&& meshEditList.Count>0){for(int i = 0;i< meshEditList.Count;i++){EditorGUILayout.ObjectField(meshEditList[i].gameObject, typeof(Object), true, GUILayout.Width(200));}}}private void ShowCtrl(){GUILayout.BeginHorizontal();GUILayout.Label("是否显示顶点色", GUILayout.Width(120));//isShowPointColor = EditorGUILayout.Toggle(isShowPointColor, GUILayout.Width(40));showPointIndex = GUILayout.SelectionGrid(showPointIndex, showPointStr, 4, GUILayout.Width(500));GUILayout.EndHorizontal();if(showPointIndex>0&& channelSelectIndex>0){GUILayout.BeginHorizontal();GUILayout.Label("显示颜色类型:", GUILayout.Width(120));showColorIndex = GUILayout.SelectionGrid(showColorIndex, showColorType, 3, GUILayout.Width(300));                GUILayout.EndHorizontal();}GUILayout.BeginHorizontal();GUILayout.Label("半径:", GUILayout.Width(80));brushSize = EditorGUILayout.Slider(brushSize, 0.1f, 2, GUILayout.Width(400));if(brushSize<0.1f){brushSize = 0.1f;}if(brushSize>2){brushSize = 10;}GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Label("衰减:", GUILayout.Width(80));brushFalloff = EditorGUILayout.Slider(brushFalloff, 0, 1, GUILayout.Width(200));if(brushFalloff<0){brushFalloff = 0;}if(brushFalloff>1){brushFalloff = 1;}GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Label("强度:", GUILayout.Width(80));brushAlpha = EditorGUILayout.Slider(brushAlpha, 0.1f, 1, GUILayout.Width(200));GUILayout.EndHorizontal();}private void ShowTitle(){channelSelectIndex = GUILayout.SelectionGrid(channelSelectIndex, channelSelectStr, 5, GUILayout.Width(500));}private void ShowContent(){if(channelSelectIndex == (int)BrushChannelType.ALL){ShowAllColorCtrl();}else{ShowOneChannelColorCtrl();}}private void ShowAllColorCtrl(){GUILayout.BeginHorizontal();GUILayout.Label("笔刷颜色:", GUILayout.Width(100));brushColor = EditorGUILayout.ColorField(brushColor, GUILayout.Width(100));GUILayout.EndHorizontal();}private void ShowOneChannelColorCtrl(){GUILayout.BeginHorizontal();GUILayout.Label("绘制模式:", GUILayout.Width(100));drawTypeIndex = GUILayout.SelectionGrid(drawTypeIndex, drawTypeStr, 2, GUILayout.Width(200));GUILayout.EndHorizontal();GUILayout.BeginHorizontal();GUILayout.Label("颜色叠加:", GUILayout.Width(100));drawAddTypeIndex = GUILayout.SelectionGrid(drawAddTypeIndex, drawAddTypeStr, 2, GUILayout.Width(200));GUILayout.EndHorizontal();}#endregion#region 显示顶点色private void ShowPointColorFunc(){if(meshEditList != null&& meshEditList.Count>0){for (int i = 0; i < meshEditList.Count; i++){ShowOneMeshPointColor(meshEditList[i]);}}}private void ShowPointColorFunc(Vector3 center,Vector3 normal){if (meshEditList != null && meshEditList.Count > 0){for (int i = 0; i < meshEditList.Count; i++){ShowOneMeshPointColor(meshEditList[i], center,normal);}}}private void ShowOneMeshPointColor(MeshEditData data){ShowOneMeshPointColor(data, Vector3.zero, Vector3.zero,false);}private void ShowOneMeshPointColor(MeshEditData data, Vector3 center,Vector3 normal,bool needCheckDistance = true){Vector3[] verts = data.mesh.vertices;float r = (brushSize + brushSize * brushFalloff);r = r * r;//Debug.Log(mesh.vertices.Length + "," + mesh.colors.Length);Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;for (int j = 0; j < verts.Length; j++){Vector3 pos = data.transform.TransformPoint(verts[j]);if (needCheckDistance==true){                    float tempR = (pos.x - center.x) * (pos.x - center.x) + (pos.y - center.y) * (pos.y - center.y) + (pos.z - center.z) * (pos.z - center.z);if (tempR > r){continue;}}if (data.colors.Length > j){if (channelSelectIndex == (int)BrushChannelType.ALL||showColorIndex ==0){Handles.color = data.colors[j];}else{if (channelSelectIndex == (int)BrushChannelType.RED){if (showColorIndex == 1){Handles.color = new Color(data.colors[j].r, 0, 0, 1);}else if (showColorIndex == 2){Handles.color = new Color(data.colors[j].r, data.colors[j].r, data.colors[j].r, 1);}}else if (channelSelectIndex == (int)BrushChannelType.GREEN){if (showColorIndex == 1){Handles.color = new Color(0, data.colors[j].g, 0, 1);}else if (showColorIndex == 2){Handles.color = new Color(data.colors[j].g, data.colors[j].g, data.colors[j].g, 1);}}else if (channelSelectIndex == (int)BrushChannelType.BLUE){if (showColorIndex == 1){Handles.color = new Color(0, 0, data.colors[j].b, 1);}else if (showColorIndex == 2){Handles.color = new Color(data.colors[j].b, data.colors[j].b, data.colors[j].b, 1);}}else if (channelSelectIndex == (int)BrushChannelType.ALPHA){Handles.color = new Color(data.colors[j].a, data.colors[j].a, data.colors[j].a, 1);}}}Handles.DotHandleCap(0, pos, Quaternion.identity, HandleUtility.GetHandleSize(pos) * 0.03f, EventType.Repaint);}}#endregion#region 绘制顶点色private void PaintPointColor(Vector3 center, Vector3 normal){if (meshEditList != null && meshEditList.Count > 0){for (int i = 0; i < meshEditList.Count; i++){PaintOneMeshPoint(meshEditList[i], center, normal);}}}private void PaintOneMeshPoint(MeshEditData data, Vector3 center, Vector3 normal){Vector3[] verts = data.mesh.vertices;float r = (brushSize + brushSize * brushFalloff);r = r * r;//Debug.Log(mesh.vertices.Length + "," + mesh.colors.Length);Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;for (int j = 0; j < verts.Length; j++){Vector3 pos = data.transform.TransformPoint(verts[j]);float tempR = (pos.x - center.x) * (pos.x - center.x) + (pos.y - center.y) * (pos.y - center.y) + (pos.z - center.z) * (pos.z - center.z);if (tempR > r){continue;}float falloff = 1;if(brushFalloff>0){falloff = Vector3.Distance(pos, center) - brushSize / (brushSize + brushSize * brushFalloff*0.1f);falloff = 1 - falloff;if(falloff<0){falloff = 0;}if(falloff>1){falloff = 1;}}if(channelSelectIndex == (int)BrushChannelType.ALL){data.DrawVertexColor(j, brushColor, falloff* brushAlpha * Time.deltaTime);}else{data.DrawVertexChannelColor(j, channelSelectIndex, drawTypeIndex, drawAddTypeIndex, falloff* brushAlpha * Time.deltaTime*0.1f);}}}#endregionvoid OnSceneGUI(SceneView sceneView){if (isEditMode == false){return;}HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));if (meshObjList == null){return;}if (HasEditableMesh() == false){return;}Event e = Event.current;if (e.isKey){//Debug.Log(e.keyCode);if (e.keyCode == KeyCode.RightBracket){brushSize += brushSizeScale;if (brushSize > 2){brushSize = 2;}}else if (e.keyCode == KeyCode.LeftBracket){brushSize -= brushSizeScale;if (brushSize < 0.1f){brushSize = 0.1f;}}else if (e.keyCode == KeyCode.Minus){brushFalloff -= 0.1f;if (brushFalloff < 0){brushFalloff = 0;}}else if (e.keyCode == KeyCode.Equals){brushFalloff += 0.1f;if (brushFalloff > 1){brushFalloff = 1;}}}if (e.rawType == EventType.MouseDown && e.button == 0){AddToUndo();isPaint = true;}if (e.rawType == EventType.MouseUp && e.button == 0){isPaint = false;}Vector3 mousePos = e.mousePosition;Camera camera = SceneView.currentDrawingSceneView.camera;float mult = 1;
#if UNITY_5_4_OR_NEWERmult = EditorGUIUtility.pixelsPerPoint;
#endifmousePos.y = camera.pixelHeight - mousePos.y * mult;mousePos.x *= mult;//Vector3 fakePoint = mousePos;//fakePoint.z = 20;//Vector3 point = sceneView.camera.ScreenToWorldPoint(fakePoint);float minDis = 99999;bool isHit = false;Vector3 center = Vector3.zero;Vector3 normal = Vector3.zero;Ray ray = camera.ScreenPointToRay(mousePos);float num = 1000;if (meshEditList != null && meshEditList.Count > 0){for (int i = 0; i < meshEditList.Count; i++){Mesh sharedMesh = meshEditList[i].mesh;RaycastHit hit;bool hasHit = IntersectRayMesh(ray, sharedMesh, meshEditList[i].transform.localToWorldMatrix, out hit);if (hasHit == false || hit.distance > num){continue;}isHit = true;if (hit.distance < minDis){center = hit.point;normal = hit.normal;minDis = hit.distance;}}}if (isHit == true){Color tempColor;if (channelSelectIndex == (int)BrushChannelType.RED){tempColor = Color.red;}else if (channelSelectIndex == (int)BrushChannelType.GREEN){tempColor = Color.green;}else if (channelSelectIndex == (int)BrushChannelType.BLUE){tempColor = Color.blue;}else if (channelSelectIndex == (int)BrushChannelType.ALPHA){tempColor = Color.gray;}else{tempColor = brushColor;}DrawBrush(tempColor, center, normal, brushSize, brushFalloff);if (showPointIndex == 1){ShowPointColorFunc(center, normal);}if (isPaint){PaintPointColor(center, normal);}sceneView.Repaint();HandleUtility.Repaint();}if (showPointIndex == 2){ShowPointColorFunc();}}#region 辅助方法private bool IntersectRayMesh(Ray ray,Mesh sharedMesh,Matrix4x4 matrix,out RaycastHit hit){System.Type type = typeof(HandleUtility);System.Reflection.MethodInfo method = type.GetMethod("IntersectRayMesh", (BindingFlags.Static | BindingFlags.NonPublic));RaycastHit tempHit = new RaycastHit();object[] objArr = new object[4];objArr[0] = ray;objArr[1] = sharedMesh;objArr[2] = matrix;objArr[3] = null;bool isSuccess = (bool)method.Invoke(null,objArr);hit = (RaycastHit)objArr[3];      return isSuccess;}private void DrawBrush(Color col, Vector3 center, Vector3 normal, float radius,float falloff){if(col.a<0.5f){col.a = 0.5f;}Handles.color = col;Handles.DrawWireDisc(center,HandleUtility.GetHandleSize(center)* normal,  radius);            Handles.DrawLine(center, center + radius * normal );if(falloff>0){Handles.color = col * 0.7f;Handles.DrawWireDisc(center, HandleUtility.GetHandleSize(center) * normal, (radius + radius* falloff));}}private bool CreateCopyMeshAsset(){if(meshObjList==null){return false;}if(HasEditableMesh() == false){return false;}string path = Application.dataPath + "/meshes";if(Directory.Exists(path)==false){Directory.CreateDirectory(path);AssetDatabase.Refresh();}bool hasError = false;Dictionary<Mesh, Mesh> dict = new Dictionary<Mesh, Mesh>();for(int i = 0;i<meshEditList.Count;i++){Mesh mesh = meshEditList[i].mesh;string meshPath = AssetDatabase.GetAssetPath(mesh);if(meshPath.EndsWith(".asset")){continue;}if(dict.ContainsKey(mesh)){meshEditList[i].SetShareMesh(dict[mesh]);}else{Mesh newMesh = CopyMesh(mesh);string savePath = GetNewSavePath(mesh.name);AssetDatabase.CreateAsset(newMesh, savePath);AssetDatabase.Refresh();Mesh loadMesh = (Mesh)AssetDatabase.LoadAssetAtPath(savePath, typeof(Mesh));dict.Add(mesh, loadMesh);meshEditList[i].SetShareMesh(loadMesh); }}if (hasError == true){AssetDatabase.SaveAssets();AssetDatabase.Refresh();return false;}AssetDatabase.SaveAssets();AssetDatabase.Refresh();return true;}private string GetNewSavePath(string assetName){string savePath = "Assets/meshes/" + assetName + ".asset";if(AssetDatabase.LoadAssetAtPath(savePath,typeof(Object))==null){return savePath;}int i = 1;while(true){i++;savePath = "Assets/meshes/" + assetName+i + ".asset";if (AssetDatabase.LoadAssetAtPath(savePath, typeof(Object)) == null){return savePath;}}}private Mesh CopyMesh(Mesh mesh){Mesh newMesh = new Mesh();newMesh.vertices = mesh.vertices;newMesh.uv = mesh.uv;newMesh.triangles = mesh.triangles;newMesh.normals = mesh.normals;if(mesh.colors.Length==0){newMesh.colors = new Color[mesh.vertices.Length];Color[] cols = new Color[mesh.vertices.Length];for (int i = 0;i<mesh.vertices.Length; i++){cols[i] = Color.black;}newMesh.SetColors(cols);}else{newMesh.colors = mesh.colors;}return newMesh;}#endregion#region Undoprivate List<DrawColorUndoData> undoList;private void AddToUndo(){if(meshEditList == null||meshEditList.Count == 0){return;}Undo.RegisterCompleteObjectUndo(this, "paint color");if (undoList == null){undoList = new List<DrawColorUndoData>();}DrawColorUndoData data = new DrawColorUndoData(meshEditList);undoList.Add(data);}private void OnUndo(){if(undoList == null||undoList.Count==0){return;}DrawColorUndoData data = undoList[undoList.Count - 1];undoList.RemoveAt(undoList.Count-1);if(meshEditList!=null&&meshEditList.Count>0){data.Undo(meshEditList);}            }#endregion#region 其他选项private void ShowTips(string str){Debug.Log(str);EditorUtility.DisplayDialog("提示", str, "确定");}private void ShowErrorTips(string str){Debug.Log(str);EditorUtility.DisplayDialog("提示", str, "确定");throw new System.Exception("errorTips:" + str);}private bool ShowSelectTips(string str,string okStr = "确定",string cancelStr = "取消"){return EditorUtility.DisplayDialog("提示", str, okStr,cancelStr);}private void ShowProgress(string title, string content, float rate){EditorUtility.DisplayProgressBar(title, content, rate);}private void HideProgress(){EditorUtility.ClearProgressBar();}private void ShowLine(string str = "", int w = -1, int h = 20){if(w<0){w = Mathf.FloorToInt(this.maxSize.x);}if (string.IsNullOrEmpty(str)){h = 5;}GUILayout.Box(str, GUILayout.Width(w), GUILayout.Height(h));}private string SelectFolder(string str){return EditorUtility.OpenFolderPanel("选择文件夹", str, "");}private string SelectFile(string str, string ex = ""){return EditorUtility.OpenFilePanel("选择文件", str, ex);}private void ShowTextArea(string str, int w = 500, int h = 60){ShowLine();GUILayout.Label(str, GUILayout.Width(w), GUILayout.Height(h));ShowLine();}#endregion}}
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace azhao.tools.VertexColorPainer
{public class MeshEditData{public Transform transform;public GameObject gameObject;private SkinnedMeshRenderer skinnedMeshRenderer;private MeshFilter meshFilter;public Mesh mesh;public Color[] colors;public MeshEditData(GameObject go){transform = go.transform;gameObject = go;skinnedMeshRenderer = go.GetComponent<SkinnedMeshRenderer>();if (skinnedMeshRenderer != null){mesh = skinnedMeshRenderer.sharedMesh;}else{meshFilter = go.GetComponent<MeshFilter>();if (meshFilter != null){mesh = meshFilter.sharedMesh;}}if (mesh != null){InitMeshColor();}}public void SetShareMesh(Mesh m){mesh = m;if(skinnedMeshRenderer!=null){skinnedMeshRenderer.sharedMesh = mesh;}if(meshFilter!=null){meshFilter.sharedMesh = mesh;}}private void InitMeshColor(){if (mesh == null){return;}int vertCount = mesh.vertexCount;colors = new Color[vertCount];for (int i = 0; i < vertCount; i++){if (mesh.colors != null && i < mesh.colors.Length){colors[i] = mesh.colors[i];}else{colors[i] = Color.black;}}}public void DrawVertexColor(int index,Color col,float alpha){if(colors == null||colors.Length<=index){return;}Color newColor = colors[index] * (1 - alpha) + col * alpha;colors[index] = newColor;
;        }public void DrawVertexChannelColor(int index,int channel,int mode,int operatorType,float alpha){if (colors == null || colors.Length <= index){return;}float val = 0;if(channel== (int)BrushChannelType.RED){val = colors[index].r;}else if(channel == (int)BrushChannelType.GREEN){val = colors[index].g;}else if (channel == (int)BrushChannelType.BLUE){val = colors[index].b;}else if (channel == (int)BrushChannelType.ALPHA){val = colors[index].a;}if(operatorType == (int)OperatorType.ADD){val += alpha;if(val>1){val = 1;}}else{val -= alpha;if(val<0){val = 0;}}if (channel == (int)BrushChannelType.RED){if(mode == (int)SetColorMode.WEIGHT){float leftVal = 1 - val;float totalVal = colors[index].g + colors[index].b + colors[index].a;if(totalVal==0){colors[index].g = colors[index].b = colors[index].a = leftVal / 3;}else{colors[index].g = leftVal * colors[index].g / totalVal;colors[index].b = leftVal * colors[index].b / totalVal;colors[index].a = leftVal * colors[index].a / totalVal;}}colors[index].r = val;}else if (channel == (int)BrushChannelType.GREEN){if (mode == (int)SetColorMode.WEIGHT){float leftVal = 1 - val;float totalVal = colors[index].r + colors[index].b + colors[index].a;if (totalVal == 0){colors[index].r = colors[index].b = colors[index].a = leftVal / 3;}else{colors[index].r = leftVal * colors[index].r / totalVal;colors[index].b = leftVal * colors[index].b / totalVal;colors[index].a = leftVal * colors[index].a / totalVal;}}colors[index].g = val;}else if (channel == (int)BrushChannelType.BLUE){if (mode == (int)SetColorMode.WEIGHT){float leftVal = 1 - val;float totalVal = colors[index].r + colors[index].g + colors[index].a;if (totalVal == 0){colors[index].r = colors[index].g = colors[index].a = leftVal / 3;}else{colors[index].r = leftVal * colors[index].r / totalVal;colors[index].g = leftVal * colors[index].g / totalVal;colors[index].a = leftVal * colors[index].a / totalVal;}}colors[index].b = val;}else if (channel == (int)BrushChannelType.ALPHA){if (mode == (int)SetColorMode.WEIGHT){float leftVal = 1 - val;float totalVal = colors[index].r + colors[index].g + colors[index].b;if (totalVal == 0){colors[index].r = colors[index].g = colors[index].b = leftVal / 3;}else{colors[index].r = leftVal * colors[index].r / totalVal;colors[index].g = leftVal * colors[index].g / totalVal;colors[index].b = leftVal * colors[index].b / totalVal;}}colors[index].a = val;}}public void SaveMesh(){if(mesh==null){return;}mesh.colors = colors;            }}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace azhao.tools.VertexColorPainer
{public class DrawColorUndoData{private List<Color[]> colors;public DrawColorUndoData(List<MeshEditData> data){colors = new List<Color[]>();for(int i = 0;i<data.Count;i++){colors.Add((Color[])data[i].colors.Clone());}}public void Undo(List<MeshEditData> data){if(colors == null||colors.Count!=data.Count){Debug.Log("xxxUndo  return");return;}for(int i = 0;i<data.Count;i++){data[i].colors = colors[i];}}
}
}

相关文章:

Unity引擎修改模型顶点色的工具

大家好&#xff0c;我是阿赵。   之前分享过怎样通过MaxScript在3DsMax里面修改模型的顶点色。不过由于很多时候顶点色的编辑需要根据在游戏引擎里面的实际情况和shader的情况来动态调整&#xff0c;所以如果能在引擎里面直接修改模型的顶点色&#xff0c;将会方便很多。于是…...

linux安装minio以及springboot整合使用

文章目录 1.linux安装minio2.springboot整合minio使用 1.linux安装minio 1.新建文件夹 mkdir /home/minio # 数据文件夹 mkdir /home/minio/data # 创建日志文件夹 mkdir /home/minio/log2.进入文件夹 cd /home/minio3.下载minio&#xff0c;链接可从官网获取 wget https://…...

javaee 事务 事务的特性 事务的并发问题 事务的隔离级别

什么是事务(Transaction) 是并发控制的单元&#xff0c;是用户定义的一个操作序列。这些操作要么都做&#xff0c;要么都不做&#xff0c;是一个不可分割的工作单位。通过事务&#xff0c;sql 能将逻辑相关的一组操作绑定在一起&#xff0c;以便服务器 保持数据的完整性。事务…...

Matlab怎么引入外部的latex包?Matlab怎么使用特殊字符?

Matlab怎么引入外部的latex包&#xff1f;Matlab怎么使用特殊字符&#xff1f; Matlab怎么使用特殊字符&#xff1f;一种是使用latex方式&#xff0c;Matlab支持基本的Latex字符【这里】&#xff0c;但一些字符需要依赖外部的包&#xff0c;例如“&#x1d53c;”&#xff0c;需…...

day-07 I/O复用(select)

一.I/O复用 &#xff08;一&#xff09;基于I/O复用的服务器端 1.多进程服务器 每次服务都需要创建一个进程&#xff0c;需要大量的运算和内存空间 2.复用 只需创建一个进程。 3.复用技术在服务器端的应用 &#xff08;二&#xff09;select函数实现服务器端 &#xff08;…...

Glide的使用及源码分析

前言 依赖 implementation com.github.bumptech.glide:glide:4.16.0 github: GitHub - bumptech/glide: An image loading and caching library for Android focused on smooth scrolling 基本使用 //加载url Glide.with(this) .load(url) .placeholder(R.drawable.placehol…...

外贸爬虫系统

全球智能搜索 全球智能搜索 支持全球所有国家搜索引擎&#xff0c;及社交平台&#xff0c;精准定位优质的外贸客户&#xff0c;免翻墙 全球任意国家地区实时采集 搜索引擎全网邮箱电话采集 社交平台一键查看采集&#xff08;Facebook,Twitter,Linkedin等&#xff09; 职位…...

CentOS 8 安装 Code Igniter 4

在安装好LNMP运行环境基础上&#xff0c;将codeigniter4文件夹移动到/var/nginx/html根目录下&#xff0c;浏览器地址栏输入IP/codeigniter/pulbic 一直提示&#xff1a; Cache unable to write to "/var/nginx/html/codeigniter/writable/cache/". 找了好久&…...

.net framework 提示安装了 但是删除面板看不到

如果你在计算机上安装了.NET Framework&#xff0c;但在“控制面板”中找不到.NET Framework的相关条目&#xff0c;可能是因为.NET Framework的某些组件或特定版本未在“程序和功能”&#xff08;或旧版本的Windows中称为“程序和功能”&#xff09;列表中列出。这可能是正常情…...

flask-smorest 库

flask-smorest 简介 flask-smorest: 基于Flask/Marshmallow的REST API框架 flask-smorest 是一个用于创建于数据库无关的REST API的架库。 它使用Flask作为Web服务器&#xff0c;并使用marsmallow对数据进行序列化和反序列化。(类似于drf) 快速入门 flask-smorest对代码应…...

android WindowManager的简单使用

<?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><uses-permission android:name"android.permis…...

Spark_Spark比mapreduce快的原因

Spark 为什么比 mapreduce 快? 最重要的3点&#xff0c; 数据缓存 : 中间结果可以缓存在内存中复用 资源管理 &#xff1a;executor task 管理&#xff0c;不同stage的task可以运行在同一个executor上 任务调度 : dag 对比多阶段mr 1.任务模型的优化&#xff08;DAG图对比…...

el-upload调用内部方法删除文件

从Element UI 的官方文档中&#xff0c; Upload 上传组组件提供了on-remove和before-remove的文件删除的钩子属性&#xff08;回调方法名&#xff09;&#xff0c;但如何调用组件删除方法&#xff08;让该方法删除本地上传文件列表以及触发这两个钩子&#xff09;并无相关说明。…...

无涯教程-JavaScript - CUBEKPIMEMBER函数

描述 该函数返回关键绩效指标(KPI)属性,并在单元格中显示KPI名称。 语法 CUBEKPIMEMBER (connection, kpi_name, kpi_property, [caption])争论 Argument描述Required/OptionalconnectionName of the connection to the cube - A text stringRequiredkpi_nameName of the K…...

代码随想录Day_52打卡

①、最长递增子序列 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序…...

692. 前K个高频单词

题目来源&#xff1a;力扣 题目描述&#xff1a; 给定一个单词列表 words 和一个整数 k &#xff0c;返回前 k 个出现次数最多的单词。 返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率&#xff0c; 按字典顺序 排序。 示例 1&#xff1a; 输入:…...

介绍 Docker 的基本概念和优势,以及在应用程序开发中的实际应用

Docker 是一个开源的容器化平台&#xff0c;可以让开发者将应用程序和其所依赖的组件&#xff08;如库、运行环境&#xff09;打包成一个可移植、自包含的容器。这个容器可以在任何支持 Docker 的环境中运行&#xff0c;包括开发、测试、生产等环境。Docker 的基本概念包括以下…...

C++:构建一个二叉树的代码

​#include <iostream>// 定义二叉树节点 struct BinaryTreeNode {int data;BinaryTreeNode* left;BinaryTreeNode* right;BinaryTreeNode(int val) : data(val), left(nullptr), right(nullptr) {} };// 构建二叉树 BinaryTreeNode* buildBinaryTree() {int val;std::ci…...

iOS 设置下载部分文件,如何获取完整文件的大小

在视频的需求中&#xff0c;遇到这样一个需求&#xff0c;播放一视频的时候&#xff0c;要预下载 后面10条视频&#xff0c;但是只下载后面十条视频的前面1M 实现方法 1 创建请求时设置cacheLength resource [[IdiotResource alloc] init];resource.requestURL task.request…...

如何助力金融贷款企业实现精准营销获客

无论是哪个行业&#xff0c;吸引客户都是核心。 许多公司的线下渠道面临着许多障碍&#xff0c;以至于他们不得不采用在线客户获取方法。受影响最大的行业之一是贷款行业。如何获得准确的贷款客户资源&#xff1f;如何赢得客户已经成为企业的一大痛点。 过去&#xff0c;信贷…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

条件运算符

C中的三目运算符&#xff08;也称条件运算符&#xff0c;英文&#xff1a;ternary operator&#xff09;是一种简洁的条件选择语句&#xff0c;语法如下&#xff1a; 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true&#xff0c;则整个表达式的结果为“表达式1”…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化

缓存架构 代码结构 代码详情 功能点&#xff1a; 多级缓存&#xff0c;先查本地缓存&#xff0c;再查Redis&#xff0c;最后才查数据库热点数据重建逻辑使用分布式锁&#xff0c;二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

Caliper 配置文件解析:fisco-bcos.json

config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合

作者&#xff1a;来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布&#xff0c;Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明&#xff0c;Elastic 作为 …...

​​企业大模型服务合规指南:深度解析备案与登记制度​​

伴随AI技术的爆炸式发展&#xff0c;尤其是大模型&#xff08;LLM&#xff09;在各行各业的深度应用和整合&#xff0c;企业利用AI技术提升效率、创新服务的步伐不断加快。无论是像DeepSeek这样的前沿技术提供者&#xff0c;还是积极拥抱AI转型的传统企业&#xff0c;在面向公众…...

【实施指南】Android客户端HTTPS双向认证实施指南

&#x1f510; 一、所需准备材料 证书文件&#xff08;6类核心文件&#xff09; 类型 格式 作用 Android端要求 CA根证书 .crt/.pem 验证服务器/客户端证书合法性 需预置到Android信任库 服务器证书 .crt 服务器身份证明 客户端需持有以验证服务器 客户端证书 .crt 客户端身份…...