using UnityEngine; using System.Collections; using UnityEditor; using System.Collections.Generic; #if __MICROSPLAT__ namespace JBooth.MicroSplat.VertexPainter { public partial class VertexPainterWindow : EditorWindow { // for external tools public System.Action OnBeginStroke; public System.Action OnStokeModified; // bool is true when doing a fill or other non-bounded opperation public System.Action OnEndStroke; // C# doesn't have *& or **, so it's not easy to pass a reference to a value for changing. // instead, we wrap the setter into a templated lambda which allows us to pass a changable // reference around via a function which sets it. Pretty tricky sis, but I'd rather just // be able to pass the freaking reference already.. // Note the ref object, which is there just to prevent boxing of Vector/Color structs. Also // note the complete lack of type safety, etc.. ugh.. // whats worse- this could also be condensed down to a macro, which would actually be MORE // safe in terms of potential bugs than all this; and it would be like a dozen lines to boot. public bool enabled; public Vector3 oldpos = Vector3.zero; public float brushSize = 1; public float brushFlow = 8; public float brushFalloff = 1; // linear public int channel = 0; public float targetValue = 1.0f; public bool showVertexPoints = false; public float showVertexSize = 1; public enum BrushVisualization { Sphere, Disk } public BrushVisualization brushVisualization = BrushVisualization.Sphere; public PaintJob[] jobs = new PaintJob[0]; // bool used to know if we've registered an undo with this object or not public bool[] jobEdits = new bool[0]; void InitMeshes() { List pjs = new List(); Object[] objs = Selection.GetFiltered(typeof(GameObject), SelectionMode.Editable | SelectionMode.Deep); for (int i = 0; i < objs.Length; ++i) { GameObject go = objs[i] as GameObject; if (go != null) { MeshFilter mf = go.GetComponent(); Renderer r = go.GetComponent(); MicroSplatVertexMesh vmesh = go.GetComponent (); if (mf != null && r != null && mf.sharedMesh != null && mf.sharedMesh.isReadable && vmesh != null) { pjs.Add(new PaintJob(mf, r, vmesh)); } } } jobs = pjs.ToArray(); jobEdits = new bool[jobs.Length]; } void SetWireframeDisplay(Renderer r, bool hidden) { EditorUtility.SetSelectedRenderState(r, hidden ? EditorSelectedRenderState.Hidden : EditorSelectedRenderState.Highlight); } void OnUndo() { for (int i = 0; i < jobs.Length; ++i) { if (jobs[i].stream != null) { jobs[i].stream.Apply(false); } } } public bool IsFXEnabled(PaintJob j) { return (j.vertexMesh.keywordSO.IsKeywordEnabled ("_STREAMS") || j.vertexMesh.keywordSO.IsKeywordEnabled ("_LAVA") || j.vertexMesh.keywordSO.IsKeywordEnabled ("_WETNESS") || j.vertexMesh.keywordSO.IsKeywordEnabled ("_PUDDLES")); } int GetChannel() { int c = channel; #if __MICROSPLAT_STREAMS__ if (tab == Tab.Wetness) { c = 28; } else if (tab == Tab.Puddles) { c = 29; } else if (tab == Tab.Streams) { c = 30; } else if (tab == Tab.Lava) { c = 31; } #endif return c; } public void FillMesh(PaintJob job) { PrepBrushMode(job); int c = GetChannel (); if (c >= 28) { for (int i = 0; i < job.verts.Length; ++i) { Color clr = job.stream.colors [i]; var uv1 = job.stream.uv1 [i]; var uv2 = job.stream.uv2 [i]; ToTemp (clr, uv1, uv2); tempWeights [c] = targetValue; FromTemp (ref clr, ref uv1, ref uv2); job.stream.colors [i] = clr; job.stream.uv1 [i] = uv1; job.stream.uv2 [i] = uv2; } } else { for (int i = 0; i < job.verts.Length; ++i) { Color clr = job.stream.colors [i]; var uv1 = job.stream.uv1 [i]; var uv2 = job.stream.uv2 [i]; ToTemp (clr, uv1, uv2); for (int x = 0; x < 28; ++x) { tempWeights [x] = 0; } tempWeights [c] = 1; FromTemp (ref clr, ref uv1, ref uv2); job.stream.colors [i] = clr; job.stream.uv1 [i] = uv1; job.stream.uv2 [i] = uv2; } } job.stream.Apply(); if (OnStokeModified != null) { OnStokeModified(job, true); } } public void InitColors(PaintJob j) { Color[] colors = j.stream.colors; if (colors == null || colors.Length != j.verts.Length) { Color[] orig = j.meshFilter.sharedMesh.colors; if (j.meshFilter.sharedMesh.colors != null && j.meshFilter.sharedMesh.colors.Length > 0) { j.stream.colors = orig; } else { j.stream.SetColor(Color.white, j.verts.Length); } } } public void InitUV1 (PaintJob j) { var uvs = j.stream.uv1; if (uvs == null || uvs.Count != j.verts.Length) { if (j.meshFilter.sharedMesh.uv2 != null && j.meshFilter.sharedMesh.uv2.Length == j.verts.Length) { List nuv = new List (j.meshFilter.sharedMesh.vertices.Length); j.meshFilter.sharedMesh.GetUVs (1, nuv); j.stream.uv1 = nuv; } else { j.stream.SetUV1 (Vector2.zero, j.verts.Length); } } } public void InitUV2 (PaintJob j) { var uvs = j.stream.uv2; if (uvs == null || uvs.Count != j.verts.Length) { if (j.meshFilter.sharedMesh.uv3 != null && j.meshFilter.sharedMesh.uv3.Length == j.verts.Length) { List nuv = new List (j.meshFilter.sharedMesh.vertices.Length); j.meshFilter.sharedMesh.GetUVs (2, nuv); j.stream.uv2 = nuv; } else { j.stream.SetUV2 (Vector2.zero, j.verts.Length); } } } public void InitPositions(PaintJob j) { Vector3[] pos = j.stream.positions; if (pos == null || pos.Length != j.verts.Length) { int vc = j.meshFilter.sharedMesh.vertexCount; if (j.stream.positions == null || j.stream.positions.Length != vc) { j.stream.positions = new Vector3[j.meshFilter.sharedMesh.vertices.Length]; j.meshFilter.sharedMesh.vertices.CopyTo(j.stream.positions, 0); } } return; } public void PrepBrushMode(PaintJob j) { InitColors (j); InitUV1 (j); InitUV2 (j); } void DrawVertexPoints(PaintJob j, Vector3 point) { if (j.HasStream() && j.HasData()) { PrepBrushMode(j); } if (j.renderer == null) { return; } // convert point into local space, so we don't have to convert every point var mtx = j.renderer.transform.localToWorldMatrix; point = j.renderer.transform.worldToLocalMatrix.MultiplyPoint3x4(point); // for some reason this doesn't handle scale, seems like it should // we handle it poorly until I can find a better solution float scale = 1.0f / Mathf.Abs(j.renderer.transform.lossyScale.x); float bz = scale * brushSize; bz *= bz; for (int i = 0; i < j.verts.Length; ++i) { //float d = Vector3.Distance(point, j.verts[i]); var p = j.verts[i]; float x = point.x - p.x; float y = point.y - p.y; float z = point.z - p.z; float dist = x * x + y * y + z * z; if (dist < bz) { Vector3 wp = mtx.MultiplyPoint(j.verts[i]); Handles.SphereHandleCap(0, point, Quaternion.identity, HandleUtility.GetHandleSize(wp) * 0.02f * showVertexSize, EventType.Repaint); } } } Color DecodeToColor (float v) { var vi = (uint)(v * (256.0f * 256.0f * 256.0f * 256.0f)); var ex = (int)(vi / (256 * 256 * 256) % 256); var ey = (int)((vi / (256 * 256)) % 256); var ez = (int)((vi / (256)) % 256); var ew = (int)(vi % 256); var e = new Color (ex / 255.0f, ey / 255.0f, ez / 255.0f, ew / 255.0f); return e; } float EncodeToFloat (Color enc) { var ex = (uint)(enc.r * 255); var ey = (uint)(enc.g * 255); var ez = (uint)(enc.b * 255); var ew = (uint)(enc.a * 255); var v = (ex << 24) + (ey << 16) + (ez << 8) + ew; return v / (256.0f * 256.0f * 256.0f * 256.0f); } float [] tempWeights = new float [32]; void ToTemp(Color c, Vector4 uv1, Vector4 uv2) { Color s0 = DecodeToColor (c.r); Color s1 = DecodeToColor (c.g); Color s2 = DecodeToColor (c.b); Color s3 = DecodeToColor (c.a); Color s4 = DecodeToColor (uv1.z); Color s5 = DecodeToColor (uv1.w); Color s6 = DecodeToColor (uv2.z); Color s7 = DecodeToColor (uv2.w); tempWeights [0] = s0.r; tempWeights [1] = s0.g; tempWeights [2] = s0.b; tempWeights [3] = s0.a; tempWeights [4] = s1.r; tempWeights [5] = s1.g; tempWeights [6] = s1.b; tempWeights [7] = s1.a; tempWeights [8] = s2.r; tempWeights [9] = s2.g; tempWeights [10] = s2.b; tempWeights [11] = s2.a; tempWeights [12] = s3.r; tempWeights [13] = s3.g; tempWeights [14] = s3.b; tempWeights [15] = s3.a; tempWeights [16] = s4.r; tempWeights [17] = s4.g; tempWeights [18] = s4.b; tempWeights [19] = s4.a; tempWeights [20] = s5.r; tempWeights [21] = s5.g; tempWeights [22] = s5.b; tempWeights [23] = s5.a; tempWeights [24] = s6.r; tempWeights [25] = s6.g; tempWeights [26] = s6.b; tempWeights [27] = s6.a; tempWeights [28] = s7.r; tempWeights [29] = s7.g; tempWeights [30] = s7.b; tempWeights [31] = s7.a; } void FromTemp(ref Color c, ref Vector4 uv1, ref Vector4 uv2) { c.r = EncodeToFloat (new Color (tempWeights [0], tempWeights [1], tempWeights [2], tempWeights [3])); c.g = EncodeToFloat (new Color (tempWeights [4], tempWeights [5], tempWeights [6], tempWeights [7])); c.b = EncodeToFloat (new Color (tempWeights [8], tempWeights [9], tempWeights [10], tempWeights [11])); c.a = EncodeToFloat (new Color (tempWeights [12], tempWeights [13], tempWeights [14], tempWeights [15])); uv1.z = EncodeToFloat (new Color (tempWeights [16], tempWeights [17], tempWeights [18], tempWeights [19])); uv1.w = EncodeToFloat (new Color (tempWeights [20], tempWeights [21], tempWeights [22], tempWeights [23])); uv2.z = EncodeToFloat (new Color (tempWeights [24], tempWeights [25], tempWeights [26], tempWeights [27])); uv2.w = EncodeToFloat (new Color (tempWeights [28], tempWeights [29], tempWeights [30], tempWeights [31])); } void NormalizeTempWeights(bool fx) { float total = 0; for (int i = 0; i < 28; ++i) { total += tempWeights [i]; } for (int i = 0; i < 28; ++i) { tempWeights [i] /= total; } } void PaintMesh(PaintJob j, Vector3 point, float value) { bool affected = false; PrepBrushMode(j); // convert point into local space, so we don't have to convert every point point = j.renderer.transform.worldToLocalMatrix.MultiplyPoint3x4(point); // for some reason this doesn't handle scale, seems like it should // we handle it poorly until I can find a better solution float scale = 1.0f / Mathf.Abs(j.renderer.transform.lossyScale.x); float bz = scale * brushSize; bz *= bz; float pressure = Event.current.pressure > 0 ? Event.current.pressure : 1.0f; bool modPos = !(j.stream.positions == null || j.stream.positions.Length == 0); { bool fx = IsFXEnabled (j); int cnl = GetChannel (); for (int i = 0; i < j.verts.Length; ++i) { Vector3 p = modPos ? j.stream.positions[i] : j.verts[i]; float x = point.x - p.x; float y = point.y - p.y; float z = point.z - p.z; float dist = x * x + y * y + z * z; if (dist < bz) { float str = 1.0f - dist / bz; str = Mathf.Pow(str, brushFalloff); float finalStr = str * (float)deltaTime * brushFlow * pressure; if (finalStr > 0) { affected = true; Color c = j.stream.colors [i]; var uv1 = j.stream.uv1 [i]; var uv2 = j.stream.uv2 [i]; ToTemp (c, uv1, uv2); var twv = tempWeights [cnl]; var after = Mathf.Clamp01(Mathf.Lerp(twv, value, finalStr)); tempWeights [cnl] = after; NormalizeTempWeights (fx); FromTemp (ref c, ref uv1, ref uv2); j.stream.colors [i] = c; j.stream.uv1 [i] = uv1; j.stream.uv2 [i] = uv2; } } } } if (affected) { j.stream.Apply(); if (OnStokeModified != null) { OnStokeModified(j, false); } } } void EndStroke() { if (OnEndStroke != null) { OnEndStroke(); } painting = false; for (int i = 0; i < jobs.Length; ++i) { PaintJob j = jobs[i]; if (j.HasStream()) { EditorUtility.SetDirty(j.stream); EditorUtility.SetDirty(j.stream.gameObject); } } } double deltaTime = 0; double lastTime = 0; bool painting = false; void DoShortcuts() { if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape) { enabled = !enabled; if (enabled) { InitMeshes(); Event.current.Use(); } } // brush adjustments const float adjustSpeed = 0.3f; if (Event.current.isKey && Event.current.type == EventType.KeyDown) { if (Event.current.keyCode == KeyCode.LeftBracket) { brushSize -= adjustSpeed; Repaint(); } else if (Event.current.keyCode == KeyCode.RightBracket) { brushSize += adjustSpeed; Repaint(); } else if (Event.current.keyCode == KeyCode.Semicolon) { brushFlow -= adjustSpeed; Repaint(); } else if (Event.current.keyCode == KeyCode.Quote) { brushFlow += adjustSpeed; Repaint(); } else if (Event.current.keyCode == KeyCode.Period) { brushFalloff -= adjustSpeed; Repaint(); } else if (Event.current.keyCode == KeyCode.Slash) { brushFlow += adjustSpeed; Repaint(); } } } void OnSceneGUI(SceneView sceneView) { DoShortcuts(); deltaTime = EditorApplication.timeSinceStartup - lastTime; lastTime = EditorApplication.timeSinceStartup; if (jobs.Length == 0 && Selection.activeGameObject != null) { InitMeshes(); } if (!enabled || jobs.Length == 0 || Selection.activeGameObject == null) { return; } if (tab == Tab.Utility) { return; } RaycastHit hit; float distance = float.MaxValue; Vector3 mousePosition = Event.current.mousePosition; // So, in 5.4, Unity added this value, which is basically a scale to mouse coordinates for retna monitors. // Not all monitors, just some of them. // What I don't get is why the fuck they don't just pass me the correct fucking value instead. I spent hours // finding this, and even the paid Unity support my company pays many thousands of dollars for had no idea // after several weeks of back and forth. If your going to fake the coordinates for some reason, please do // it everywhere to not just randomly break things everywhere you don't multiply some new value in. float mult = EditorGUIUtility.pixelsPerPoint; mousePosition.y = sceneView.camera.pixelHeight - mousePosition.y * mult; mousePosition.x *= mult; Vector3 fakeMP = mousePosition; fakeMP.z = 20; Vector3 point = sceneView.camera.ScreenToWorldPoint(fakeMP); Vector3 normal = Vector3.forward; Ray ray = sceneView.camera.ScreenPointToRay(mousePosition); bool registerUndo = (Event.current.type == EventType.MouseDown && Event.current.button == 0 && Event.current.alt == false); bool toggleWireframe = (Event.current.type == EventType.KeyUp && Event.current.control); for (int i = 0; i < jobs.Length; ++i) { if (jobs[i] == null || jobs[i].meshFilter == null) continue; // Early out if we're not in the area.. Bounds b = jobs[i].renderer.bounds; b.Expand(brushSize*2); if (!b.IntersectRay(ray)) { continue; } if (registerUndo) { painting = true; // clear job edits for (int x = 0; x < jobEdits.Length; ++x) { jobEdits[x] = false; } if (OnBeginStroke != null) { OnBeginStroke(jobs); } } if (toggleWireframe) { SetWireframeDisplay(jobs[i].renderer, hideMeshWireframe); } Matrix4x4 mtx = jobs[i].meshFilter.transform.localToWorldMatrix; Mesh msh = jobs[i].meshFilter.sharedMesh; if (jobs[i].HasStream()) { msh = jobs[i].stream.GetModifierMesh(); } if (msh == null) { msh = jobs[i].meshFilter.sharedMesh; } if (RXLookingGlass.IntersectRayMesh(ray, msh, mtx, out hit)) { if (Event.current.shift == false) { if (hit.distance < distance) { distance = hit.distance; point = hit.point; oldpos = hit.point; normal = hit.normal; // if we don't have normal overrides, we have to recast against the shared mesh to get it's normal // This could get a little strange if you modify the mesh, then delete the normal data, but in that // case there's no real correct answer anyway without knowing the index of the vertex we're hitting. if (normal.magnitude < 0.1f) { RXLookingGlass.IntersectRayMesh(ray, jobs[i].meshFilter.sharedMesh, mtx, out hit); normal = hit.normal; } } } else { point = oldpos; } } else { if (Event.current.shift == true) { point = oldpos; } } } if (Event.current.type == EventType.MouseMove && Event.current.shift) { brushSize += Event.current.delta.x * (float)deltaTime * 6.0f; brushFalloff -= Event.current.delta.y * (float)deltaTime * 48.0f; } if (Event.current.rawType == EventType.MouseUp) { EndStroke(); } if (Event.current.type == EventType.MouseMove && Event.current.alt) { brushSize += Event.current.delta.y * (float)deltaTime; } Handles.color = new Color(1, 0, 0, 0.4f); if (brushVisualization == BrushVisualization.Sphere) { Handles.SphereHandleCap(0, point, Quaternion.identity, brushSize * 2, EventType.Repaint); } else { Handles.color = new Color(0.8f, 0, 0, 1.0f); float r = Mathf.Pow(0.5f, brushFalloff); Handles.DrawWireDisc(point, normal, brushSize * r); Handles.color = new Color(0.9f, 0, 0, 0.8f); Handles.DrawWireDisc(point, normal, brushSize); } // eat current event if mouse event and we're painting if (Event.current.isMouse && painting) { Event.current.Use(); } if (Event.current.type == EventType.Layout) { HandleUtility.AddDefaultControl(GUIUtility.GetControlID(GetHashCode(), FocusType.Passive)); } if (jobs.Length > 0 && painting) { for (int i = 0; i < jobs.Length; ++i) { Bounds b = jobs[i].renderer.bounds; b.Expand(brushSize*2); if (!b.IntersectRay(ray)) { continue; } if (jobEdits[i] == false) { jobEdits[i] = true; Undo.RegisterCompleteObjectUndo(jobs[i].stream, "Vertex Painter Stroke"); } PaintMesh(jobs[i], point, targetValue); Undo.RecordObject(jobs[i].stream, "Vertex Painter Stroke"); } } if (jobs.Length > 0 && showVertexPoints) { for (int i = 0; i < jobs.Length; ++i) { DrawVertexPoints(jobs[i], point); } } // update views sceneView.Repaint(); HandleUtility.Repaint(); } } } #endif