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

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,链接可从官网获取 wget https://…...

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

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

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

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…...

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

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

.net framework 提示安装了 但是删除面板看不到
如果你在计算机上安装了.NET Framework,但在“控制面板”中找不到.NET Framework的相关条目,可能是因为.NET Framework的某些组件或特定版本未在“程序和功能”(或旧版本的Windows中称为“程序和功能”)列表中列出。这可能是正常情…...

flask-smorest 库
flask-smorest 简介 flask-smorest: 基于Flask/Marshmallow的REST API框架 flask-smorest 是一个用于创建于数据库无关的REST API的架库。 它使用Flask作为Web服务器,并使用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点, 数据缓存 : 中间结果可以缓存在内存中复用 资源管理 :executor task 管理,不同stage的task可以运行在同一个executor上 任务调度 : dag 对比多阶段mr 1.任务模型的优化(DAG图对比…...

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

无涯教程-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 ,找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序…...

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

介绍 Docker 的基本概念和优势,以及在应用程序开发中的实际应用
Docker 是一个开源的容器化平台,可以让开发者将应用程序和其所依赖的组件(如库、运行环境)打包成一个可移植、自包含的容器。这个容器可以在任何支持 Docker 的环境中运行,包括开发、测试、生产等环境。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 设置下载部分文件,如何获取完整文件的大小
在视频的需求中,遇到这样一个需求,播放一视频的时候,要预下载 后面10条视频,但是只下载后面十条视频的前面1M 实现方法 1 创建请求时设置cacheLength resource [[IdiotResource alloc] init];resource.requestURL task.request…...

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

html中的换行(\n)或回车(\r)符号不起作用的解决办法、br、white、space、pre、line
文章目录 前言html不解释空白字符CSS样式的影响特定的文本区域white-space 前言 如果您的HTML中的换行和回车符号(\n和\r)似乎不起作用,可能有几种可能的原因: html不解释空白字符 在HTML中,连续的空白字符࿰…...

SpringBoot+MyBatisPlus+MySql+vue2+elementUi的案例、java访问数据库服务、java提供接口服务
文章目录 前言后端关键代码前端关键代码完整代码 前言 1、项目不使用前后端分离。 2、在创建SpringBoot的时候要注意各个插件间的版本问题。 3、后端技术SpringBootMyBatisPlusMySql。 4、前端技术vue2elementUi。 后端关键代码 简单介绍 1、数据库名称ssm_db 2、表名称tbl_bo…...

设计模式入门(二)观察者模式
设计模式入门 本系列所有内容参考自《HeadFirst设计模式》。因为书中的代码是采用java语言写的,博主这里用C语言改写。 这里采用讲故事的方式进行讲解。若有错误之处,非常欢迎大家指导。 设计模式:模式不是代码,而针对设计问题的…...

列化复杂的xml对应的类
你能帮我写一个序列化复杂的xml对应的类吗? c#代码 好的,可以试着为一个复杂的XML生成对应的C#类。这里我就简单的XML为例: <Book><Name>Design Patterns</Name><Author>Erich Gamma</Author><Pages>395</Pages><Chapters>…...

什么是软件开发生命周期(SDLC)?
软件开发生命周期(SDLC)指的是从软件项目开始到最终交付的整个过程。它是软件开发过程的指导框架,用于规划、开发、测试、部署和维护软件系统。 SDLC包含了一系列阶段,每个阶段都有特定的任务、活动和产物。这些阶段通常包括以下…...

计算机视觉中常用的角点检测算法及其作用
角点检测是计算机视觉中的重要任务,用于识别图像中的角点或关键点。以下是一些常用的角点检测算法: Harris角点检测:Harris角点检测是一种经典的角点检测算法,它通过计算图像中每个像素的角点响应函数来检测角点。Harris角点检测对…...

css3英文文字换行,超过两行...展示
需求:超过两行...展示 开发的过程中发现div内容中文可以换行英文不换行,导致长度会溢出。 是英文全英文的话浏览器会解析成一个单词, 加上这句就好了 word-break:break-all; 一开始不知道是会解析成一个单词,用字符串拼接处理…...

查各种金属非金属材料的物性参数方法
背景 上面给了任务,要做调研,各种材料的各种参数,高温的、低温的、常温的、常压的、高压的、低压的。 网上搜出来很多材料的参数都是各种卖材料的厂商给出的,也不晓得他们的测量结果可不可信,有没有一个权威机构背书…...

【数据库】查询PostgreSQL中所有表逻辑外键
引言 在PostgreSQL数据库中,逻辑外键是用于约束表之间关系的一种机制。然而,在某些情况下,我们可能需要删除和重建逻辑外键。本文将介绍如何查询PostgreSQL中所有表的逻辑外键,并指导您如何先删除再重新建立这些外键。 查询Post…...

【Kubernetes理论篇】2023年最新CKA考题+解析
文章目录 第一题:RBAC授权访问控制第二题:Node节点维护第三题:K8S集群版本升级第四题:ETCD数据库备份恢复第五题:NetworkPolicy网络策略第六题:Service四层负载第七题:Ingress七层负载第八题&am…...