using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MTE.Undo;
using UnityEditor;
using UnityEngine;
using System.Runtime.CompilerServices;
using static MTE.TextureArrayShaderPropertyNames;
using System.Runtime.InteropServices;
namespace MTE
{
///
/// Texture-array-based Mesh-Terrain texture editor.
///
internal class TextureArrayPainter : IEditor
{
public int Id { get; } = 10;
public bool Enabled { get; set; } = true;
public string Name { get; } = nameof(TextureArrayPainter);
public Texture Icon { get; } =
EditorGUIUtility.IconContent("TerrainInspector.TerrainToolSplat").image;
public string Header { get { return StringTable.Get(C.TextureArrayPainter_Header); } }
public string Description { get { return StringTable.Get(C.TextureArrayPainter_Description); } }
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;
// min/max
const float MinBrushSize = 0.1f;
const float MaxBrushSize = 10f;
const float MinBrushFlow = 0.01f;
const float MaxBrushFlow = 1f;
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_TextureArrayPainter.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_TextureArrayPainter.brushFlow", value);
}
}
}
#endregion
#region UI
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)),
};
#endregion
public TextureArrayPainter()
{
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 (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()
{
painterMode = (EditorFilterMode)EditorPrefs.GetInt(
"MTE_TextureArrayPainter.painterMode", (int)DefaultPainterMode);
brushSize = EditorPrefs.GetFloat("MTE_TextureArrayPainter.brushSize", DefaultBrushSize);
brushFlow = EditorPrefs.GetFloat("MTE_TextureArrayPainter.brushFlow", DefaultBrushFlow);
}
private GameObject targetGameObject { get; set; }
private Mesh targetMesh { get; set; }
private Material targetMaterial { get; set; }
private Texture2D[] controlTextures { get; } = new Texture2D[3] {null, null, null};
private void BuildEditingInfoForLegacyMode(GameObject gameObject)
{
//reset
this.TextureList.Clear();
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 (!material)
{
return;
}
if (!MTEShaders.IsMTETextureArrayShader(material.shader))
{
return;
}
//collect targets info
this.targetGameObject = gameObject;
this.targetMaterial = material;
this.targetMesh = meshFilter.sharedMesh;
// Texture
LoadTextureList();
LoadControlTextures();
// Preview
if (TextureList.Count != 0)
{
if (SelectedTextureIndex < 0 || SelectedTextureIndex > TextureList.Count - 1)
{
SelectedTextureIndex = 0;
}
preview.LoadPreview(TextureList[SelectedTextureIndex],
BrushSizeInU3D,
BrushIndex);
}
}
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())
{
LoadTextureList();
if (PainterMode == EditorFilterMode.SelectedGameObject)
{
BuildEditingInfoForLegacyMode(Selection.activeGameObject);
}
LoadPreview();
}
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));
//TODO use texture-array version message
}
else
{
EditorGUILayout.LabelField(
StringTable.Get(C.Info_TextureArrayPainter_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);
//Tools
if (!Settings.CompactGUI)
{
EditorGUILayout.Space();
GUILayout.Label(StringTable.Get(C.Tools), MTEStyles.SubHeader);
}
EditorGUILayout.BeginVertical();
{
EditorGUILayout.BeginHorizontal();
{
if (GUILayout.Button(StringTable.Get(C.CreateTextureArraySettings),
GUILayout.Width(100), GUILayout.Height(40)))
{
EditorApplication.ExecuteMenuItem(
$"Assets/Create/Mesh Terrain Editor/{nameof(TextureArraySettings)}");
}
GUILayout.Space(20);
EditorGUILayout.LabelField(
StringTable.Get(C.Info_ToolDescription_CreateTextureArraySettings),
MTEStyles.labelFieldWordwrap);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
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;
LoadPreview();
});
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(3);
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;
}
if (targetGameObject != raycastHit.transform.gameObject)
{
return;
}
var currentBrushSize = BrushSizeInU3D/2;
if (Settings.ShowBrushRect)
{
Utility.ShowBrushRect(raycastHit.point, currentBrushSize);
}
var controlIndex = SelectedTextureIndex / 4;
Debug.Assert(0 <= controlIndex && controlIndex <= 3);
var controlTexture = controlTextures[controlIndex];
if (controlTexture == null)
{
throw new MTEEditException("The control texture at index {controlIndex} is null.");
}
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 pixelUV = raycastHit.textureCoord;
var pX = Mathf.FloorToInt(pixelUV.x * controlWidth);
var pY = Mathf.FloorToInt(pixelUV.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(ControlTexturePropertyNames[0]))
{
Texture2D texture = (Texture2D) material.GetTexture(ControlTexturePropertyNames[0]);
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(ControlTexturePropertyNames[1]))
{
Texture2D texture = (Texture2D) material.GetTexture(ControlTexturePropertyNames[1]);
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(ControlTexturePropertyNames[2]))
{
Texture2D texture = (Texture2D) material.GetTexture(ControlTexturePropertyNames[2]);
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.");
//Convention: for texture array, the selected texture index is the layer index
if (!material.HasProperty(AlbedoArrayPropertyName))
{
//zwcloud/MeshTerrainEditor-issues#218
var relativePath = AssetDatabase.GetAssetPath(material);
MTEDebug.LogError(
$"Material<{material.name}> at <{relativePath}> using shader <{material.shader.name}> doesn't have a texture property" +
$" '{AlbedoArrayPropertyName}'. Please capture a screenshot of the material editor, and report this issue with it.");
return;
}
Texture2DArray textureArray =
material.GetTexture(AlbedoArrayPropertyName) as Texture2DArray;
var layerIndex = TextureArrayManager.Instance.GetTextureSliceIndex(textureArray,
selectedTexture);
if (layerIndex < 0)
{
continue;
}
//MTEDebug.Log("get number of layer-textures in the material.");
int splatTotal = GetLayerTextureNumber(material);
//MTEDebug.Log("check control textures.");
// fetch control textures from material, TODO refator to merge duplicate code below
Texture2D controlTexture0 = null, controlTexture1 = null, controlTexture2 = null;
if (splatTotal > 0)//controlTexture0 should exists
{
if (!material.HasProperty(ControlTexturePropertyNames[0]))
{//impossible if using a builtin TextureArray shader
throw new MTEEditException($"Property {ControlTexturePropertyNames[0]} " +
$"doesn't exist in material<{material.name}>.");
}
var tex = material.GetTexture(ControlTexturePropertyNames[0]);
if (tex != null)
{
controlTexture0 = (Texture2D)tex;
}
else
{
throw new MTEEditException($"{ControlTexturePropertyNames[0]} is" +
$" not assigned or existing in material<{material.name}>.");
}
}
if (splatTotal > 4)//controlTexture1 should exists
{
if (!material.HasProperty(ControlTexturePropertyNames[1]))
{//impossible if using a builtin TextureArray shader
throw new MTEEditException($"Property {ControlTexturePropertyNames[1]} " +
$"doesn't exist in material<{material.name}>.");
}
var tex = material.GetTexture(ControlTexturePropertyNames[1]);
if (tex == null)
{
throw new MTEEditException($"Property {ControlTexturePropertyNames[1]} " +
$"is not assigned in material<{material.name}>.");
}
controlTexture1 = (Texture2D)tex;
}
if (splatTotal > 8)//controlTexture2 should exists
{
if (!material.HasProperty(ControlTexturePropertyNames[2]))
{//impossible if using a builtin TextureArray shader
throw new MTEEditException($"Property {ControlTexturePropertyNames[2]} " +
$"doesn't exist in material<{material.name}>.");
}
var tex = material.GetTexture(ControlTexturePropertyNames[2]);
if (tex == null)
{
throw new MTEEditException($"{ControlTexturePropertyNames[2]} " +
$"is not assigned in material<{material.name}>.");
}
controlTexture2 = (Texture2D)tex;
}
// check which control texture is to be modified
Texture2D controlTexture = controlTexture0;
if (layerIndex >= 4)
{
controlTexture = controlTexture1;
}
if (layerIndex >= 8)
{
controlTexture = controlTexture2;
}
if (controlTexture1 != null)
{
if (controlTexture0.width != controlTexture1.width)
{
throw new MTEEditException(
$"Size of {controlTexture0.name} is different from other control textures." +
"Make sure all control textures is the same size.");
}
}
if (controlTexture2 != null)
{
if (controlTexture0.width != controlTexture2.width)
{
throw new MTEEditException(
$"Size of {controlTexture2.name} is different from other control textures." +
"Make sure all control textures is the same size.");
}
}
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, layerIndex, splatTotal,
controlTexture0, controlTexture1, controlTexture2,
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)
{
//record values before modification for undo
foreach (var modifyGroup in modifyGroups)
{
var gameObject = modifyGroup.gameObject;
var material = gameObject.GetComponent().sharedMaterial;
if (material.HasProperty(ControlTexturePropertyNames[0]))
{
Texture2D texture = (Texture2D)material.GetTexture(ControlTexturePropertyNames[0]);
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(ControlTexturePropertyNames[1]))
{
Texture2D texture = (Texture2D) material.GetTexture(ControlTexturePropertyNames[1]);
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(ControlTexturePropertyNames[2]))
{
Texture2D texture = (Texture2D) material.GetTexture(ControlTexturePropertyNames[2]);
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;
//set all control textures readable, just in case
Utility.SetTextureReadable(material.GetTexture(ControlTexturePropertyNames[0]));
if (material.HasProperty(ControlTexturePropertyNames[1]))
{
Utility.SetTextureReadable(material.GetTexture(ControlTexturePropertyNames[1]));
}
if (material.HasProperty(ControlTexturePropertyNames[2]))
{
Utility.SetTextureReadable(material.GetTexture(ControlTexturePropertyNames[2]));
}
PaintTexture(modifyGroup.controlTexture0,
modifyGroup.controlTexture1,
modifyGroup.controlTexture2,
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 void PaintTexture(Texture2D controlTexture0, Texture2D controlTexture1,
Texture2D controlTexture2, int splatIndex, int splatTotal, Rect texelRect,
Vector2 minUV, Vector2 maxUV)
{
// check parameters
if (splatTotal > 0 && controlTexture0 == null)
{
throw new System.ArgumentException(
$"[MTE] {nameof(controlTexture0)} is null.",
nameof(controlTexture0));
}
if (splatTotal > 4 && controlTexture1 == null)
{
throw new System.ArgumentException(
$"[MTE] splatIndex is 4/5/6/7 but {nameof(controlTexture1)} is null.",
nameof(controlTexture1));
}
if (splatTotal > 8 && controlTexture2 == null)
{
throw new System.ArgumentException(
$"[MTE] splatIndex is 8/9/10/11 but {nameof(controlTexture2)} is null.",
nameof(controlTexture2));
}
if (splatIndex < 0 || splatIndex > 11)
{
throw new System.ArgumentOutOfRangeException(nameof(splatIndex), splatIndex,
"[MTE] splatIndex should be [0, 11].");
}
// 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));
}
else
{
modifyingSections.Add(Array.Empty());
}
if (controlTexture2 != null)
{
modifyingSections.Add(controlTexture2.GetPixels(x, y, width, height, 0));
}
else
{
modifyingSections.Add(Array.Empty());
}
// 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
Utility.BlendPixelSections(BrushFlow, BrushStrength, modifyingSections,
splatIndex, splatTotal, height, width);
// modify the control texture
if (splatTotal > 8)
{
controlTexture0.SetPixels(x, y, width, height, modifyingSections[0]);
controlTexture0.Apply();
System.Diagnostics.Debug.Assert(controlTexture1 != null);
System.Diagnostics.Debug.Assert(modifyingSections[1].Length != 0);
controlTexture1.SetPixels(x, y, width, height, modifyingSections[1]);
controlTexture1.Apply();
System.Diagnostics.Debug.Assert(controlTexture2 != null);
System.Diagnostics.Debug.Assert(modifyingSections[2].Length != 0);
controlTexture2.SetPixels(x, y, width, height, modifyingSections[2]);
controlTexture2.Apply();
DirtyTextureSet.Add(controlTexture0);
DirtyTextureSet.Add(controlTexture1);
DirtyTextureSet.Add(controlTexture2);
}
else if(splatTotal > 4)
{
controlTexture0.SetPixels(x, y, width, height, modifyingSections[0]);
controlTexture0.Apply();
System.Diagnostics.Debug.Assert(controlTexture1 != null);
System.Diagnostics.Debug.Assert(modifyingSections[1].Length != 0);
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 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 System.ArgumentNullException("texture");
}
var path = AssetDatabase.GetAssetPath(texture);
var bytes = texture.EncodeToPNG();
if(bytes == null || bytes.Length == 0)
{
throw new System.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: true);
//Don't modify this field, it's used by MTE editors internally
public List TextureList = new List(16);
///
/// load all splat textures form targets
///
private 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($"Material<{material.name}> doesn't use a valid shader!");
return;
}
if (!MTEShaders.IsMTETextureArrayShader(shader))
{
MTEDebug.LogWarning(
$"Material<{material.name}> doesn't use a MTE TextureArray shader!");
return;
}
RuntimeTextureArrayLoader runtimeTextureArrayLoader =
meshRenderer.GetComponent();
TextureArraySettings settings = null;
if (runtimeTextureArrayLoader)
{
//create and assign texture array to material
runtimeTextureArrayLoader.LoadInEditor();
settings = runtimeTextureArrayLoader.settings;
}
var textureArray = material.GetTexture(AlbedoArrayPropertyName) as Texture2DArray;
if (textureArray == null || textureArray.depth <= 0)
{
MTEDebug.LogWarning(
$"Material<{material.name}>'s {AlbedoArrayPropertyName} property is empty.");
return;
}
TextureArrayManager.Instance.AddOrUpdate(textureArray, settings);
TextureArrayManager.Instance.GetTextures(textureArray, out var textures);
var propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int j = 0; j < propertyCount; j++)
{
if (ShaderUtil.GetPropertyType(shader, j) != ShaderUtil.ShaderPropertyType.TexEnv)
{
continue;
}
var propertyName = ShaderUtil.GetPropertyName(shader, j);
if (propertyName != AlbedoArrayPropertyName)
{
continue;
}
foreach (var texture in textures)
{
if (!TextureList.Contains(texture))
{
TextureList.Add(texture);
}
}
}
}
private void LoadControlTextures()
{
if (!targetMaterial)
{
return;
}
int splatTotal = GetLayerTextureNumber(targetMaterial);
int weightMapTotal = splatTotal / 4;
Debug.Assert(weightMapTotal <= ControlTexturePropertyNames.Length);
int width = -1, height = -1;
for (int weightMapIndex = 0; weightMapIndex < weightMapTotal; weightMapIndex++)
{
var controlPropertyName = ControlTexturePropertyNames[weightMapIndex];
var controlTexture = targetMaterial.GetTexture(controlPropertyName) as Texture2D;
if (controlTexture == null )
{
throw new MTEEditException($"Property {controlPropertyName} isn't assigned " +
$"or doesn't exist in material<{targetMaterial.name}>.");
}
var controlTextureWidth = controlTexture.width;
var controlTextureHeight = controlTexture.height;
if (controlTextureWidth != controlTextureHeight)
{
throw new MTEEditException($"{controlPropertyName} texture is not square.");
}
if (width < 0 && height < 0)
{
width = controlTextureWidth;
height = controlTextureHeight;
}
if (width != controlTextureWidth || height != controlTextureHeight)
{
throw new MTEEditException(
$"Size of {controlPropertyName} is different from others.");
}
controlTextures[weightMapIndex] = controlTexture;
}
}
private static bool CheckIfMaterialAssetPathAvailable(Material material)
{
var relativePathOfMaterial = AssetDatabase.GetAssetPath(material);
if (relativePathOfMaterial.StartsWith("Resources"))
{//built-in material
return false;
}
return true;
}
private static int GetLayerTextureNumber(Material material)
{
if (material.IsKeywordEnabled(KeyWords.HasWeightMap2))
{
return 12;
}
if (material.IsKeywordEnabled(KeyWords.HasWeightMap1))
{
return 8;
}
return 4;
}
}
}