using System; using System.Collections.Generic; using System.IO; using MTE.Undo; using UnityEditor; using UnityEngine; namespace MTE { /// /// Splat texture painter /// /// /// naming convention: /// control textures, "_Control" and "_ControlExtra" /// splat textures, "_Splat0/1/2/3/4/5/6/7" /// Only the last few splat textures can be null. /// Painting textures will normalize all control textures' color rects. /// internal class SplatPainter : IEditor { private static readonly GUIContent[] EditorFilterModeContents = { new GUIContent(StringTable.Get(C.SplatPainter_Mode_Filtered), StringTable.Get(C.SplatPainter_Mode_FilteredDescription)), new GUIContent(StringTable.Get(C.SplatPainter_Mode_Selected), StringTable.Get(C.SplatPainter_Mode_SelectedDescription)), }; public int Id { get; } = 4; public bool Enabled { get; set; } = true; public string Name { get; } = "SplatPainter"; public Texture Icon { get; } = EditorGUIUtility.IconContent("TerrainInspector.TerrainToolSplat").image; public bool WantMouseMove { get; } = true; public bool WillEditMesh { get; } = false; #region Parameters #region Constant // default const EditorFilterMode DefaultPainterMode = EditorFilterMode.FilteredGameObjects; const float DefaultBrushSize = 1; const float DefaultBrushFlow = 0.5f; const float DefaultBrushAlpha = 0.5f; // min/max const float MinBrushSize = 0.1f; const float MaxBrushSize = 10f; const float MinBrushFlow = 0.01f; const float MaxBrushFlow = 1f; const float MinBrushAlpha = 0.5f; const float MaxBrushAlpha = 0.5f; const int MaxHotkeyNumberForTexture = 8; #endregion public int brushIndex; public float brushSize; public float brushFlow; private int selectedTextureIndex; private EditorFilterMode painterMode; private EditorFilterMode PainterMode { get { return this.painterMode; } set { if (value != this.painterMode) { EditorPrefs.SetInt("MTE_SplatPainter.painterMode", (int)value); this.painterMode = value; } } } /// /// Index of selected texture in the texture list; not the layer index. /// public int SelectedTextureIndex { get { return this.selectedTextureIndex; } set { var textureListCount = TextureList.Count; if (value < textureListCount) { this.selectedTextureIndex = value; } } } /// /// Index of selected brush /// public int BrushIndex { get { return brushIndex; } set { if (brushIndex != value) { preview.SetPreviewMaskTexture(value); brushIndex = value; } } } /// /// Brush size (unit: 1 BrushUnit) /// public float BrushSize { get { return brushSize; } set { value = Mathf.Clamp(value, MinBrushSize, MaxBrushSize); if (!MathEx.AmostEqual(brushSize, value)) { brushSize = value; EditorPrefs.SetFloat("MTE_SplatPainter.brushSize", value); if (PainterMode == EditorFilterMode.FilteredGameObjects) { preview.SetPreviewSize(BrushSizeInU3D/2); } else { //preview size for SelectedGameObject mode are set in OnSceneGUI } } } } //real brush size private float BrushSizeInU3D { get { return BrushSize * Settings.BrushUnit; } } /// /// Brush flow /// public float BrushFlow { get { return brushFlow; } set { value = Mathf.Clamp(value, MinBrushFlow, MaxBrushFlow); if (Mathf.Abs(brushFlow - value) > 0.0001f) { brushFlow = value; EditorPrefs.SetFloat("MTE_SplatPainter.brushFlow", value); } } } #endregion public SplatPainter() { MTEContext.EnableEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); LoadTextureList(); if (PainterMode == EditorFilterMode.SelectedGameObject) { BuildEditingInfoForLegacyMode(Selection.activeGameObject); } if (TextureList.Count != 0) { if (SelectedTextureIndex < 0) { SelectedTextureIndex = 0; } LoadPreview(); } } }; MTEContext.EditTypeChangedEvent += (sender, args) => { if (MTEContext.editor == this) { LoadSavedParamter(); LoadTextureList(); if (PainterMode == EditorFilterMode.SelectedGameObject) { BuildEditingInfoForLegacyMode(Selection.activeGameObject); } if (TextureList.Count != 0) { if (SelectedTextureIndex < 0 || SelectedTextureIndex > TextureList.Count - 1) { SelectedTextureIndex = 0; } LoadPreview(); } } else { if (preview != null) { preview.UnLoadPreview(); } } }; MTEContext.SelectionChangedEvent += (sender, args) => { if (MTEContext.editor == this) { if (args.SelectedGameObject) { if (PainterMode == EditorFilterMode.SelectedGameObject) { BuildEditingInfoForLegacyMode(args.SelectedGameObject); } } } }; MTEContext.TextureChangedEvent += (sender, args) => { if (MTEContext.editor == this) { LoadTextureList(); if (PainterMode == EditorFilterMode.SelectedGameObject) { BuildEditingInfoForLegacyMode(Selection.activeGameObject); } } }; MTEContext.DisableEvent += (sender, args) => { if (preview != null) { preview.UnLoadPreview(); } }; MTEContext.EditTargetsLoadedEvent += (sender, args) => { if (MTEContext.editor == this) { LoadTextureList(); } }; // Load default parameters painterMode = DefaultPainterMode; brushSize = DefaultBrushSize; brushFlow = DefaultBrushFlow; } private void LoadPreview() { var texture = TextureList[SelectedTextureIndex]; preview.LoadPreview(texture, BrushSizeInU3D, BrushIndex); } private void LoadSavedParamter() { // Load parameters from the EditorPrefs painterMode = (EditorFilterMode)EditorPrefs.GetInt( "MTE_SplatPainter.painterMode", (int)DefaultPainterMode); brushSize = EditorPrefs.GetFloat("MTE_SplatPainter.brushSize", DefaultBrushSize); brushFlow = EditorPrefs.GetFloat("MTE_SplatPainter.brushFlow", DefaultBrushFlow); } private GameObject targetGameObject { get; set; } private Mesh targetMesh { get; set; } private Material targetMaterial { get; set; } private Texture2D[] controlTextures { get; } = new Texture2D[2] {null, null}; private void BuildEditingInfoForLegacyMode(GameObject gameObject) { //reset this.TextureList.Clear(); this.controlTextures[0] = null; this.controlTextures[1] = null; this.targetGameObject = null; this.targetMaterial = null; this.targetMesh = null; //check gameObject if (!gameObject) { return; } if (PainterMode != EditorFilterMode.SelectedGameObject) { return; } var meshFilter = gameObject.GetComponent(); if (!meshFilter) { return; } var meshRenderer = gameObject.GetComponent(); if (!meshRenderer) { return; } var material = meshRenderer.sharedMaterial; if (!meshRenderer) { return; } if (MTEShaders.IsMTETextureArrayShader(material.shader)) { return; } //collect targets info this.targetGameObject = gameObject; this.targetMaterial = material; this.targetMesh = meshFilter.sharedMesh; // splat textures LoadTextureList(); LoadControlTextures(); if (controlTextures[0] == null) { return; } // Preview if (TextureList.Count != 0) { if (SelectedTextureIndex < 0 || SelectedTextureIndex > TextureList.Count - 1) { SelectedTextureIndex = 0; } LoadPreview(); } } public string Header { get { return StringTable.Get(C.SplatPainter_Header); } } public string Description { get { return StringTable.Get(C.SplatPainter_Description); } } private static class Styles { public static string NoGameObjectSelectedHintText; private static bool unloaded= true; public static void Init() { if (!unloaded) return; NoGameObjectSelectedHintText = StringTable.Get(C.Info_PleaseSelectAGameObjectWithVaildMesh); unloaded = false; } } public void DoArgsGUI() { Styles.Init(); EditorGUI.BeginChangeCheck(); this.PainterMode = (EditorFilterMode)GUILayout.Toolbar( (int)this.PainterMode, EditorFilterModeContents); if (EditorGUI.EndChangeCheck() && PainterMode == EditorFilterMode.SelectedGameObject) { BuildEditingInfoForLegacyMode(Selection.activeGameObject); } if (PainterMode == EditorFilterMode.SelectedGameObject && Selection.activeGameObject == null) { EditorGUILayout.HelpBox(Styles.NoGameObjectSelectedHintText, MessageType.Warning); return; } BrushIndex = Utility.ShowBrushes(BrushIndex); // Splat-textures if (!Settings.CompactGUI) { GUILayout.Label(StringTable.Get(C.Textures), MTEStyles.SubHeader); } EditorGUILayout.BeginVertical("box"); { var textureListCount = TextureList.Count; if (textureListCount == 0) { if (PainterMode == EditorFilterMode.FilteredGameObjects) { EditorGUILayout.LabelField( StringTable.Get(C.Info_SplatPainter_NoSplatTextureFound), GUILayout.Height(64)); } else { EditorGUILayout.LabelField( StringTable.Get(C.Info_SplatPainter_NoSplatTextureFoundOnSelectedObject), GUILayout.Height(64)); } } else { for (int i = 0; i < textureListCount; i += 4) { EditorGUILayout.BeginHorizontal(); { var oldBgColor = GUI.backgroundColor; for (int j = 0; j < 4; j++) { if (i + j >= textureListCount) break; EditorGUILayout.BeginVertical(); var texture = TextureList[i + j]; bool toggleOn = SelectedTextureIndex == i + j; if (toggleOn) { GUI.backgroundColor = new Color(62 / 255.0f, 125 / 255.0f, 231 / 255.0f); } GUIContent toggleContent; if (i + j + 1 <= MaxHotkeyNumberForTexture) { toggleContent = new GUIContent(texture, StringTable.Get(C.Hotkey) + ':' + StringTable.Get(C.NumPad) + (i + j + 1)); } else { toggleContent = new GUIContent(texture); } var new_toggleOn = GUILayout.Toggle(toggleOn, toggleContent, GUI.skin.button, GUILayout.Width(64), GUILayout.Height(64)); GUI.backgroundColor = oldBgColor; if (new_toggleOn && !toggleOn) { SelectedTextureIndex = i + j; // reload the preview if (PainterMode == EditorFilterMode.SelectedGameObject) { preview.LoadPreviewFromObject(texture, BrushSizeInU3D, BrushIndex, targetGameObject); } else { preview.LoadPreview(texture, BrushSizeInU3D, BrushIndex); } } EditorGUILayout.EndVertical(); } } EditorGUILayout.EndHorizontal(); } } } EditorGUILayout.EndVertical(); //Settings if (!Settings.CompactGUI) { EditorGUILayout.Space(); GUILayout.Label(StringTable.Get(C.Settings), MTEStyles.SubHeader); } BrushSize = EditorGUILayoutEx.Slider(StringTable.Get(C.Size), "-", "+", BrushSize, MinBrushSize, MaxBrushSize); BrushFlow = EditorGUILayoutEx.SliderLog10(StringTable.Get(C.Flow), "[", "]", BrushFlow, MinBrushFlow, MaxBrushFlow); GUILayout.FlexibleSpace(); EditorGUILayout.HelpBox(StringTable.Get(C.Info_WillBeSavedInstantly), MessageType.Info, true); } public HashSet DefineHotkeys() { var hashSet = new HashSet { new Hotkey(this, KeyCode.Minus, () => { BrushFlow -= 0.01f; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.Equals, () => { BrushFlow += 0.01f; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.LeftBracket, () => { BrushSize -= 1; MTEEditorWindow.Instance.Repaint(); }), new Hotkey(this, KeyCode.RightBracket, () => { BrushSize += 1; MTEEditorWindow.Instance.Repaint(); }), }; for (int i = 0; i < MaxHotkeyNumberForTexture; i++) { int index = i; var hotkey = new Hotkey(this, KeyCode.Keypad0+index+1, () => { SelectedTextureIndex = index; // reload the preview if (PainterMode == EditorFilterMode.SelectedGameObject) { preview.LoadPreviewFromObject(TextureList[SelectedTextureIndex], BrushSizeInU3D, BrushIndex, targetGameObject); } else { preview.LoadPreview(TextureList[SelectedTextureIndex], BrushSizeInU3D, BrushIndex); } MTEEditorWindow.Instance.Repaint(); }); hashSet.Add(hotkey); } return hashSet; } // buffers of editing helpers private readonly List modifyGroups = new List(4); private float[] BrushStrength = new float[1024 * 1024];//buffer for brush blending to forbid re-allocate big array every frame when painting. private readonly List modifyingSections = new List(2); private UndoTransaction currentUndoTransaction; public void OnSceneGUI() { var e = Event.current; if (preview == null || !preview.IsReady || TextureList.Count == 0) { return; } if (e.commandName == "UndoRedoPerformed") { SceneView.RepaintAll(); return; } if (!(EditorWindow.mouseOverWindow is SceneView)) { return; } // do nothing when mouse middle/right button, control/alt key is pressed if (e.button != 0 || e.control || e.alt) return; HandleUtility.AddDefaultControl(0); var ray = HandleUtility.GUIPointToWorldRay(e.mousePosition); RaycastHit raycastHit; if (PainterMode == EditorFilterMode.SelectedGameObject) { if (!targetGameObject || !targetMaterial || !targetMesh) { return; } if (!Physics.Raycast(ray, out raycastHit, Mathf.Infinity, ~targetGameObject.layer)) { return; } var currentBrushSize = BrushSizeInU3D/2; if (Settings.ShowBrushRect) { Utility.ShowBrushRect(raycastHit.point, currentBrushSize); } var controlIndex = SelectedTextureIndex / 4; var controlTexture = controlTextures[controlIndex]; var controlWidth = controlTexture.width; var controlHeight = controlTexture.height; var meshSize = targetGameObject.GetComponent().bounds.size.x; var brushSizeInTexel = (int) Mathf.Round(BrushSizeInU3D/meshSize*controlWidth); preview.SetNormalizedBrushSize(BrushSizeInU3D/meshSize); preview.SetNormalizedBrushCenter(raycastHit.textureCoord); preview.SetPreviewSize(BrushSizeInU3D/2); preview.MoveTo(raycastHit.point); SceneView.RepaintAll(); if ((e.type == EventType.MouseDrag && e.alt == false && e.shift == false && e.button == 0) || (e.type == EventType.MouseDown && e.shift == false && e.alt == false && e.button == 0)) { // 1. Collect all sections to be modified var sections = new List(); var texelUV = raycastHit.textureCoord; var pX = Mathf.FloorToInt(texelUV.x * controlWidth); var pY = Mathf.FloorToInt(texelUV.y * controlHeight); var x = Mathf.Clamp(pX - brushSizeInTexel / 2, 0, controlWidth - 1); var y = Mathf.Clamp(pY - brushSizeInTexel / 2, 0, controlHeight - 1); var width = Mathf.Clamp((pX + brushSizeInTexel / 2), 0, controlWidth) - x; var height = Mathf.Clamp((pY + brushSizeInTexel / 2), 0, controlHeight) - y; for (var i = 0; i < controlTextures.Length; i++) { var texture = controlTextures[i]; if (texture == null) continue; sections.Add(texture.GetPixels(x, y, width, height, 0)); } // 2. Modify target var replaced = sections[controlIndex]; var maskTexture = (Texture2D) MTEStyles.brushTextures[BrushIndex]; BrushStrength = new float[brushSizeInTexel * brushSizeInTexel]; for (var i = 0; i < brushSizeInTexel; i++) { for (var j = 0; j < brushSizeInTexel; j++) { BrushStrength[j * brushSizeInTexel + i] = maskTexture.GetPixelBilinear(((float) i) / brushSizeInTexel, ((float) j) / brushSizeInTexel).a; } } var controlColor = new Color(); controlColor[SelectedTextureIndex % 4] = 1.0f; for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { var index = (i * width) + j; var Stronger = BrushStrength[ Mathf.Clamp((y + i) - (pY - brushSizeInTexel / 2), 0, brushSizeInTexel - 1) * brushSizeInTexel + Mathf.Clamp((x + j) - (pX - brushSizeInTexel / 2), 0, brushSizeInTexel - 1)] * BrushFlow; replaced[index] = Color.Lerp(replaced[index], controlColor, Stronger); } } if (e.type == EventType.MouseDown) { using (new UndoTransaction()) { var material = targetMaterial; if (material.HasProperty("_Control")) { Texture2D texture = (Texture2D) material.GetTexture("_Control"); if (texture != null) { var originalColors = texture.GetPixels(); UndoRedoManager.Instance().Push(a => { texture.ModifyPixels(a); texture.Apply(); Save(texture); }, originalColors, "Paint control texture"); } } if (material.HasProperty("_ControlExtra")) { Texture2D texture = (Texture2D) material.GetTexture("_ControlExtra"); if (texture != null) { var originalColors = texture.GetPixels(); UndoRedoManager.Instance().Push(a => { texture.ModifyPixels(a); texture.Apply(); Save(texture); }, originalColors, "Paint control texture"); } } } } controlTexture.SetPixels(x, y, width, height, replaced); controlTexture.Apply(); // 3. Normalize other control textures NormalizeWeightsLegacy(sections); for (var i = 0; i < controlTextures.Length; i++) { var texture = controlTextures[i]; if (texture == null) { continue; } if (texture == controlTexture) { continue; } texture.SetPixels(x, y, width, height, sections[i]); texture.Apply(); } } else if (e.type == EventType.MouseUp && e.alt == false && e.button == 0) { foreach (var texture in controlTextures) { if (texture) { Save(texture); } } } } else { if(Physics.Raycast(ray, out raycastHit, Mathf.Infinity, 1 << MTEContext.TargetLayer//only hit target layer )) { //check tag if (!raycastHit.transform.CompareTag(MTEContext.TargetTag)) { return; } var currentBrushSize = BrushSizeInU3D; if (Settings.ShowBrushRect) { Utility.ShowBrushRect(raycastHit.point, currentBrushSize/2); } var hitPoint = raycastHit.point; preview.MoveTo(hitPoint); float meshSize = 1.0f; // collect modify group modifyGroups.Clear(); foreach (var target in MTEContext.Targets) { //MTEDebug.Log("Check if we can paint on target."); var meshRenderer = target.GetComponent(); if (meshRenderer == null) continue; var meshFilter = target.GetComponent(); if (meshFilter == null) continue; var mesh = meshFilter.sharedMesh; if (mesh == null) continue; Vector2 textureUVMin;//min texture uv that is to be modified Vector2 textureUVMax;//max texture uv that is to be modified Vector2 brushUVMin;//min brush mask uv that will be used Vector2 brushUVMax;//max brush mask uv that will be used { //MTEDebug.Log("Start: Check if they intersect with each other."); // check if the brush rect intersects with the `Mesh.bounds` of this target var hitPointLocal = target.transform.InverseTransformPoint(hitPoint);//convert hit point from world space to target mesh space Bounds brushBounds = new Bounds(center: new Vector3(hitPointLocal.x, 0, hitPointLocal.z), size: new Vector3(currentBrushSize, 99999, currentBrushSize)); Bounds meshBounds = mesh.bounds;//TODO rename this Bounds paintingBounds; var intersected = meshBounds.Intersect(brushBounds, out paintingBounds); if(!intersected) continue; Vector2 paintingBounds2D_min = new Vector2(paintingBounds.min.x, paintingBounds.min.z); Vector2 paintingBounds2D_max = new Vector2(paintingBounds.max.x, paintingBounds.max.z); //calculate which part of control texture should be modified Vector2 meshRendererBounds2D_min = new Vector2(meshBounds.min.x, meshBounds.min.z); Vector2 meshRendererBounds2D_max = new Vector2(meshBounds.max.x, meshBounds.max.z); textureUVMin = MathEx.NormalizeTo01(rangeMin: meshRendererBounds2D_min, rangeMax: meshRendererBounds2D_max, value: paintingBounds2D_min); textureUVMax = MathEx.NormalizeTo01(rangeMin: meshRendererBounds2D_min, rangeMax: meshRendererBounds2D_max, value: paintingBounds2D_max); if (target.transform == raycastHit.transform) { meshSize = meshBounds.size.x; } //calculate which part of brush mask texture should be used Vector2 brushBounds2D_min = new Vector2(brushBounds.min.x, brushBounds.min.z); Vector2 brushBounds2D_max = new Vector2(brushBounds.max.x, brushBounds.max.z); brushUVMin = MathEx.NormalizeTo01(rangeMin: brushBounds2D_min, rangeMax: brushBounds2D_max, value: paintingBounds2D_min); brushUVMax = MathEx.NormalizeTo01(rangeMin: brushBounds2D_min, rangeMax: brushBounds2D_max, value: paintingBounds2D_max); if (Settings.DebugMode) { Handles.color = Color.blue; HandlesEx.DrawRectangle(paintingBounds2D_min, paintingBounds2D_max); Handles.color = new Color(255, 128, 166); HandlesEx.DrawRectangle(meshRendererBounds2D_min, meshRendererBounds2D_max); Handles.color = Color.green; HandlesEx.DrawRectangle(brushBounds2D_min, brushBounds2D_max); } //MTEDebug.Log("End: Check if they intersect with each other."); } if (e.button == 0 && (e.type == EventType.MouseDown || e.type == EventType.MouseDrag)) { //MTEDebug.Log("Start handling mouse down."); // find the splat-texture in the material, get the X (splatIndex) from `_SplatX` var selectedTexture = TextureList[SelectedTextureIndex]; var material = meshRenderer.sharedMaterial; if (material == null) { MTEDebug.LogError("Failed to find material on target GameObject's MeshRenderer. " + "The first material on the MeshRenderer should be editable by MTE."); return; } //MTEDebug.Log("Finding the selected texture in the material."); var splatIndex = material.FindSplatTexture(selectedTexture); if (splatIndex < 0) { continue; } //MTEDebug.Log("get number of splat-textures in the material."); int splatTotal = GetSplatTextureCount(material); //MTEDebug.Log("check control textures."); // check control textures var controlTexture0_ = material.GetTexture("_Control"); Texture2D controlTexture0 = null, controlTexture1 = null; if (controlTexture0_ != null) { controlTexture0 = (Texture2D)controlTexture0_; } else { throw new InvalidOperationException(string.Format("[MTE] \"_Control\" is not assigned or existing in material<{0}>.", material.name)); } if (material.HasProperty("_ControlExtra")) { var controlTexture1_ = material.GetTexture("_ControlExtra"); if (controlTexture1_ == null) { throw new InvalidOperationException(string.Format("[MTE] \"_ControlExtra\" is not assigned or existing in material<{0}>.", material.name)); } controlTexture1 = (Texture2D)controlTexture1_; } // check which control texture is to be modified Texture2D controlTexture = controlTexture0; if (splatIndex >= 4) { controlTexture = controlTexture1; } System.Diagnostics.Debug.Assert(controlTexture != null, "controlTexture != null"); //get modifying texel rect of the control texture int x = (int)Mathf.Clamp(textureUVMin.x * (controlTexture.width - 1), 0, controlTexture.width - 1); int y = (int)Mathf.Clamp(textureUVMin.y * (controlTexture.height - 1), 0, controlTexture.height - 1); int width = Mathf.Clamp(Mathf.FloorToInt(textureUVMax.x * controlTexture.width) - x, 0, controlTexture.width - x); int height = Mathf.Clamp(Mathf.FloorToInt(textureUVMax.y * controlTexture.height) - y, 0, controlTexture.height - y); var texelRect = new Rect(x, y, width, height); modifyGroups.Add(new TextureModifyGroup(target, splatIndex, splatTotal, controlTexture0, controlTexture1, texelRect, brushUVMin, brushUVMax)); //MTEDebug.Log("End handling mouse down."); } } preview.SetNormalizedBrushSize(BrushSizeInU3D/meshSize); preview.SetNormalizedBrushCenter(raycastHit.textureCoord); //record undo operation for targets that to be modified if (e.button == 0 && e.type == EventType.MouseDown) { currentUndoTransaction = new UndoTransaction("Paint Texture"); } if (currentUndoTransaction != null && e.button == 0 && e.type==EventType.MouseDown) { foreach (var modifyGroup in modifyGroups) { var gameObject = modifyGroup.gameObject; var material = gameObject.GetComponent().sharedMaterial; if (material.HasProperty("_Control")) { Texture2D texture = (Texture2D)material.GetTexture("_Control"); if (texture != null) { var originalColors = texture.GetPixels(); UndoRedoManager.Instance().Push(a => { texture.ModifyPixels(a); texture.Apply(); Save(texture); }, originalColors, "Paint control texture"); } } if (material.HasProperty("_ControlExtra")) { Texture2D texture = (Texture2D) material.GetTexture("_ControlExtra"); if (texture != null) { var originalColors = texture.GetPixels(); UndoRedoManager.Instance().Push(a => { texture.ModifyPixels(a); texture.Apply(); Save(texture); }, originalColors, "Paint control texture"); } } } } if (e.button == 0 && e.type == EventType.MouseUp) { Debug.Assert(currentUndoTransaction != null); currentUndoTransaction.Dispose(); } // execute the modification if (modifyGroups.Count != 0) { for (int i = 0; i < modifyGroups.Count; i++) { var modifyGroup = modifyGroups[i]; var gameObject = modifyGroup.gameObject; var material = gameObject.GetComponent().sharedMaterial; Utility.SetTextureReadable(material.GetTexture("_Control")); if (material.HasProperty("_ControlExtra")) { Utility.SetTextureReadable(material.GetTexture("_ControlExtra")); } PaintTexture(modifyGroup.controlTexture0, modifyGroup.controlTexture1, modifyGroup.splatIndex, modifyGroup.splatTotal, modifyGroup.texelRect, modifyGroup.minUV, modifyGroup.maxUV); } } // auto save when mouse up if (e.type == EventType.MouseUp && e.button == 0) { foreach (var texture2D in DirtyTextureSet) { Save(texture2D); } DirtyTextureSet.Clear(); } } } SceneView.RepaintAll(); } private static int GetSplatTextureCount(Material material) { var hasPackedSplat012 = material.HasProperty("_PackedSplat0") && material.HasProperty("_PackedSplat1") && material.HasProperty("_PackedSplat2"); var hasPackedSplat345 = material.HasProperty("_PackedSplat3") && material.HasProperty("_PackedSplat4") && material.HasProperty("_PackedSplat5"); if(hasPackedSplat012) { if (hasPackedSplat345) { return 8; } return 4; } int splatTotal; if (material.HasProperty("_Splat7")) { splatTotal = 8; } else if (material.HasProperty("_Splat6")) { splatTotal = 7; } else if (material.HasProperty("_Splat5")) { splatTotal = 6; } else if (material.HasProperty("_Splat4")) { splatTotal = 5; } else if (material.HasProperty("_Splat3")) { splatTotal = 4; } else if (material.HasProperty("_Splat2")) { splatTotal = 3; } else if (material.HasProperty("_Splat1")) { splatTotal = 2; } else { throw new InvalidShaderException( "[MTE] Cannot find property _Splat1/2/3/4/5/6/7 or _PackedSplat0/1/2/3/4/5 in shader."); } return splatTotal; } private void PaintTexture(Texture2D controlTexture0, Texture2D controlTexture1, int splatIndex, int splatTotal, Rect texelRect, Vector2 minUV, Vector2 maxUV) { // check parameters if (controlTexture0 == null) { throw new ArgumentNullException("controlTexture0"); } if (splatIndex > 3 && controlTexture1 == null) { throw new ArgumentException("[MTE] splatIndex is 4/5/6/7 but controlTexture1 is null.", "controlTexture1"); } if (splatIndex < 0 || splatIndex > 7) { throw new ArgumentOutOfRangeException("splatIndex", splatIndex, "splatIndex should be 0/1/2/3/4/5/6/7."); } // collect the pixel sections to modify modifyingSections.Clear(); int x = (int)texelRect.x; int y = (int)texelRect.y; int width = (int)texelRect.width; int height = (int)texelRect.height; modifyingSections.Add(controlTexture0.GetPixels(x, y, width, height, 0)); if (controlTexture1 != null) { modifyingSections.Add(controlTexture1.GetPixels(x, y, width, height, 0)); } // sample brush strength from the mask texture var maskTexture = (Texture2D) MTEStyles.brushTextures[BrushIndex]; if (BrushStrength.Length < width*height)//enlarge buffer if it is not big enough { BrushStrength = new float[width * height]; } var unitUV_u = (maxUV.x - minUV.x)/(width-1); if (width == 1) { unitUV_u = maxUV.x - minUV.x; } var unitUV_v = (maxUV.y - minUV.y)/(height-1); if (height == 1) { unitUV_v = maxUV.y - minUV.y; } for (var i = 0; i < height; i++) { float v = minUV.y + i * unitUV_v; for (var j = 0; j < width; j++) { var pixelIndex = i * width + j; float u = minUV.x + j * unitUV_u; BrushStrength[pixelIndex] = maskTexture.GetPixelBilinear(u, v).a; } } // blend the pixel section for (var i = 0; i < height; i++) { for (var j = 0; j < width; j++) { var pixelIndex = i * width + j; var factor = BrushStrength[pixelIndex] * BrushFlow; var oldWeight = GetWeight(modifyingSections, j, i, width, splatIndex); var newWeight = Mathf.Lerp(oldWeight, 1, factor); SetWeight(modifyingSections, j, i, width, splatIndex, newWeight); NormalizeWeights(j, i, width, splatIndex, splatTotal); } } // modify the control texture if(splatTotal >= 5) { controlTexture0.SetPixels(x, y, width, height, modifyingSections[0]); controlTexture0.Apply(); System.Diagnostics.Debug.Assert(controlTexture1 != null, nameof(controlTexture1) + " != null"); controlTexture1.SetPixels(x, y, width, height, modifyingSections[1]); controlTexture1.Apply(); DirtyTextureSet.Add(controlTexture0); DirtyTextureSet.Add(controlTexture1); } else { controlTexture0.SetPixels(x, y, width, height, modifyingSections[0]); controlTexture0.Apply(); DirtyTextureSet.Add(controlTexture0); } } private static float GetWeight(List colorData, int x, int y, int width, int splatIndex) { var colors = colorData[splatIndex / 4]; var weight = colors[y * width + x][splatIndex % 4]; return weight; } private static void SetWeight(List colorData, int x, int y, int width, int splatIndex, float weight) { var colors = colorData[splatIndex / 4]; var color = colors[y * width + x]; color[splatIndex % 4] = weight; colors[y * width + x] = color; } private void NormalizeWeights(int x, int y, int width, int splatIndex, int splatTotal) { float newWeight = GetWeight(modifyingSections, x, y, width, splatIndex); float otherWeights = 0; for (int i = 0; i < splatTotal; i++) { if (i != splatIndex) { otherWeights += GetWeight(modifyingSections, x, y, width, i); } } if (otherWeights >= 1/255.0f) { float k = (1 - newWeight) / otherWeights; for (int i = 0; i < splatTotal; i++) { if (i != splatIndex) { var weight = k * GetWeight(modifyingSections, x, y, width, i); SetWeight(modifyingSections, x, y, width, i, weight); } } } else { for (int i = 0; i < splatTotal; i++) { var weight = (i == splatIndex) ? 1 : 0; SetWeight(modifyingSections, x, y, width, i, weight); } } } private void NormalizeWeightsLegacy(List sections) { var colorCount = sections[0].Length; for (var i = 0; i < colorCount; i++) { var total = 0f; for (var j = 0; j < sections.Count; j++) { var color = sections[j][i]; total += color[0] + color[1] + color[2] + color[3]; if(j == SelectedTextureIndex/4) { total -= color[SelectedTextureIndex%4]; } } if(total > 0.01) { var a = sections[SelectedTextureIndex/4][i][SelectedTextureIndex%4]; var k = (1 - a)/total; for (var j = 0; j < sections.Count; j++) { for (var l = 0; l < 4; l++) { if(!(j == SelectedTextureIndex/4 && l == SelectedTextureIndex%4)) { sections[j][i][l] *= k; } } } } else { for (var j = 0; j < sections.Count; j++) { sections[j][i][SelectedTextureIndex%4] = (j != SelectedTextureIndex/4) ? 0 : 1; } } } } public static readonly HashSet DirtyTextureSet = new HashSet(); private static void Save(Texture2D texture) { if(texture == null) { throw new ArgumentNullException("texture"); } var path = AssetDatabase.GetAssetPath(texture); var bytes = texture.EncodeToPNG(); if(bytes == null || bytes.Length == 0) { throw new Exception("[MTE] Failed to save texture to png file."); } File.WriteAllBytes(path, bytes); MTEDebug.LogFormat("Texture<{0}> saved to <{1}>.", texture.name, path); } private Preview preview = new Preview(isArray: false); //Don't modify this field, it's used by MTE editors internally public List TextureList = new List(16); /// /// load all splat textures form targets /// public void LoadTextureList() { TextureList.Clear(); if (painterMode == EditorFilterMode.SelectedGameObject) { MTEDebug.Log("Loading layer textures on selected GameObject..."); LoadTargetTextures(targetGameObject); } else { MTEDebug.Log("Loading layer textures on target GameObject(s)..."); foreach (var target in MTEContext.Targets) { LoadTargetTextures(target); } } // make collected splat textures readable Utility.SetTextureReadable(TextureList, true); MTEDebug.LogFormat("{0} layer textures loaded.", TextureList.Count); } private void LoadTargetTextures(GameObject target) { if (!target) { return; } var meshRenderer = target.GetComponent(); if (meshRenderer == null) { return; } var material = meshRenderer.sharedMaterial; if (!material) { return; } if (!CheckIfMaterialAssetPathAvailable(material)) { return; } Shader shader = material.shader; if (shader == null) { MTEDebug.LogWarning(string.Format("The material<{0}> isn't using a valid shader!", material.name)); return; } //regular shaders: find textures from shader properties var propertyCount = ShaderUtil.GetPropertyCount(shader); for (int j = 0; j < propertyCount; j++) { if (ShaderUtil.GetPropertyType(shader, j) == ShaderUtil.ShaderPropertyType.TexEnv) { var propertyName = ShaderUtil.GetPropertyName(shader, j); //propertyName should be _Splat0/1/2/3/4 if (propertyName.StartsWith("_Splat")) { var texture = material.GetTexture(propertyName); if (texture is Texture2DArray) { continue; } if (texture != null && !TextureList.Contains(texture)) { TextureList.Add(texture); } } } } //packed shaders: find textures from related parameter asset var parameters = material.LoadPackedShaderGUIParameters(); if (parameters != null) { foreach (var texture in parameters.SplatTextures) { if (texture != null && !TextureList.Contains(texture)) { TextureList.Add(texture); } } } } private void LoadControlTextures() { if (!targetMaterial) { return; } var material = targetMaterial; Texture controlTexture0_ = null; if (material.HasProperty("_Control")) { controlTexture0_ = material.GetTexture("_Control"); } if (controlTexture0_ != null) { controlTextures[0] = (Texture2D)controlTexture0_; } else { MTEDebug.LogWarning( $"[MTE] \"_Control\" is not assigned or existing in material<{material.name}>."); } if (material.HasProperty("_ControlExtra")) { var controlTexture1_ = material.GetTexture("_ControlExtra"); if (controlTexture1_ == null) { MTEDebug.LogWarning( $"[MTE] \"_ControlExtra\" is not assigned or existing in material<{material.name}>."); } else { controlTextures[1] = (Texture2D)controlTexture1_; } } } private static bool CheckIfMaterialAssetPathAvailable(Material material) { var relativePathOfMaterial = AssetDatabase.GetAssetPath(material); if (relativePathOfMaterial.StartsWith("Resources")) {//built-in material return false; } return true; } } }