//#define PB_RENDER_PICKER_TEXTURE using System.Collections.Generic; using System.Linq; using UObject = UnityEngine.Object; namespace UnityEngine.ProBuilder { /// /// A collection of settings that define how mesh element picking behaves. /// public struct PickerOptions { /// /// Gets or sets whether to perform depth testing when testing elements with raycasting. /// /// /// True to select only visible elements; false to select all elements regardless of visibility. /// public bool depthTest { get; set; } /// /// Gets or sets whether to require elements to be completely encompassed by the `rect` selection /// (RectSelectMode.Complete) or to allow elements only partially inside the `rect` selection (RectSelectMode.Partial). /// /// /// Does not apply to vertex picking. /// public RectSelectMode rectSelectMode { get; set; } static readonly PickerOptions k_Default = new PickerOptions() { depthTest = true, rectSelectMode = RectSelectMode.Partial, }; /// /// Represents a set of picking options with default values. By default, the property /// is set to true and the property is set to . /// public static PickerOptions Default { get { return k_Default; } } /// /// Evaluates whether the specified object is equivalent to this one. /// /// The object to compare to this object. /// True if both objects are PickerOptions and their property values match; false otherwise. public override bool Equals(object obj) { if (!(obj is PickerOptions)) return false; return Equals((PickerOptions)obj); } /// /// Evaluates whether the specified PickerOptions object contains the same settings as this one has. /// /// The PickerOptions object to compare to this object. /// True if the and property values match; false otherwise. public bool Equals(PickerOptions other) { return depthTest == other.depthTest && rectSelectMode == other.rectSelectMode; } /// /// Returns the hash code for this object. /// /// A hash code for the current object. public override int GetHashCode() { unchecked { return (depthTest.GetHashCode() * 397) ^ (int)rectSelectMode; } } /// /// Returns true if the two PickerOptions objects have matching values. /// /// The first object to compare. /// The second object to compare. /// True if the objects have matching values; false otherwise. public static bool operator==(PickerOptions a, PickerOptions b) { return a.Equals(b); } /// /// Returns true if the values for the two PickerOptions objects don't match. /// /// The first object to compare. /// The second object to compare. /// True if the values for the two PickerOptions objects don't match; false otherwise. public static bool operator!=(PickerOptions a, PickerOptions b) { return !a.Equals(b); } } /// /// Provides functions for picking elements in a view by rendering a picker texture and testing pixels. /// static partial class SelectionPickerRenderer { internal interface ISelectionPickerRenderer { Texture2D RenderLookupTexture(Camera camera, Shader shader, string tag, int width, int height); } const string k_FacePickerOcclusionTintUniform = "_Tint"; static readonly Color k_Blackf = new Color(0f, 0f, 0f, 1f); static readonly Color k_Whitef = new Color(1f, 1f, 1f, 1f); const uint k_PickerHashNone = 0x00; const uint k_PickerHashMin = 0x1; const uint k_PickerHashMax = 0x00FFFFFF; const uint k_MinEdgePixelsForValidSelection = 1; static bool s_Initialized = false; static ISelectionPickerRenderer s_PickerRenderer = null; static RenderTextureFormat renderTextureFormat { get { if (s_Initialized) return s_RenderTextureFormat; s_Initialized = true; for (int i = 0; i < s_PreferredFormats.Length; i++) { if (SystemInfo.SupportsRenderTextureFormat(s_PreferredFormats[i])) { s_RenderTextureFormat = s_PreferredFormats[i]; break; } } return s_RenderTextureFormat; } } static TextureFormat textureFormat { get { return TextureFormat.ARGB32; } } static RenderTextureFormat s_RenderTextureFormat = RenderTextureFormat.Default; static RenderTextureFormat[] s_PreferredFormats = new RenderTextureFormat[] { RenderTextureFormat.ARGB32, RenderTextureFormat.ARGBFloat, }; /// /// Returns an appropriate implementation based on the graphic pipeline /// to generate the lookup texture. /// URP and Standard pipeline share the same picker implementation for now. /// static ISelectionPickerRenderer pickerRenderer { get { if (s_PickerRenderer == null) s_PickerRenderer = ShouldUseHDRP()? (ISelectionPickerRenderer)new SelectionPickerRendererHDRP() : new SelectionPickerRendererStandard(); return s_PickerRenderer; } } /// /// Given a camera and selection rect (in screen space) return a Dictionary containing the number of faces touched by the rect. /// /// /// /// /// /// /// public static Dictionary> PickFacesInRect( Camera camera, Rect pickerRect, IList selection, int renderTextureWidth = -1, int renderTextureHeight = -1) { Dictionary> map; Texture2D tex = RenderSelectionPickerTexture(camera, selection, out map, renderTextureWidth, renderTextureHeight); Color32[] pix = tex.GetPixels32(); int ox = System.Math.Max(0, Mathf.FloorToInt(pickerRect.x)); int oy = System.Math.Max(0, Mathf.FloorToInt((tex.height - pickerRect.y) - pickerRect.height)); int imageWidth = tex.width; int imageHeight = tex.height; int width = Mathf.FloorToInt(pickerRect.width); int height = Mathf.FloorToInt(pickerRect.height); UObject.DestroyImmediate(tex); Dictionary> selected = new Dictionary>(); SimpleTuple hit; HashSet faces = null; HashSet used = new HashSet(); #if PB_RENDER_PICKER_TEXTURE List rectImg = new List(); #endif for (int y = oy; y < System.Math.Min(oy + height, imageHeight); y++) { for (int x = ox; x < System.Math.Min(ox + width, imageWidth); x++) { #if PB_RENDER_PICKER_TEXTURE rectImg.Add(pix[y * imageWidth + x]); #endif uint v = SelectionPickerRenderer.DecodeRGBA(pix[y * imageWidth + x]); if (used.Add(v) && map.TryGetValue(v, out hit)) { if (selected.TryGetValue(hit.item1, out faces)) faces.Add(hit.item2); else selected.Add(hit.item1, new HashSet() { hit.item2 }); } } } #if PB_RENDER_PICKER_TEXTURE if (width > 0 && height > 0) { // Debug.Log("used: \n" + used.Select(x => string.Format("{0} ({1})", x, EncodeRGBA(x))).ToString("\n")); Texture2D img = new Texture2D(width, height); img.SetPixels(rectImg.ToArray()); img.Apply(); byte[] bytes = img.EncodeToPNG(); System.IO.File.WriteAllBytes("Assets/rect.png", bytes); #if UNITY_EDITOR UnityEditor.AssetDatabase.Refresh(); #endif UObject.DestroyImmediate(img); } #endif return selected; } /// /// Selects the vertex indexes contained within a rect (rectangular selection). /// /// Use this camera to evaluate whether any vertices are inside the `rect`. /// Rect is in GUI space, where 0,0 is the top left of the screen, and `width = cam.pixelWidth / pointsPerPixel`. /// The collection of objects to check for selectability. ProBuilder verifies whether these objects fall inside the `rect`. /// True to include mesh elements that are hidden; false otherwise /// Width of the texture. If not specified, ProBuilder uses auto sizing (matches the camera's texture width). /// Height of the texture. If not specified, ProBuilder uses auto sizing (matches the camera's texture height). /// A dictionary of ProBuilderMesh objects and vertex indices that are in the selection `rect`. public static Dictionary> PickVerticesInRect( Camera camera, Rect pickerRect, IList selection, bool doDepthTest, int renderTextureWidth = -1, int renderTextureHeight = -1) { Dictionary> map; Dictionary> selected = new Dictionary>(); #if PB_RENDER_PICKER_TEXTURE List rectImg = new List(); #endif Texture2D tex = RenderSelectionPickerTexture(camera, selection, doDepthTest, out map, renderTextureWidth, renderTextureHeight); Color32[] pix = tex.GetPixels32(); int ox = System.Math.Max(0, Mathf.FloorToInt(pickerRect.x)); int oy = System.Math.Max(0, Mathf.FloorToInt((tex.height - pickerRect.y) - pickerRect.height)); int imageWidth = tex.width; int imageHeight = tex.height; int width = Mathf.FloorToInt(pickerRect.width); int height = Mathf.FloorToInt(pickerRect.height); UObject.DestroyImmediate(tex); SimpleTuple hit; HashSet indexes = null; HashSet used = new HashSet(); for (int y = oy; y < System.Math.Min(oy + height, imageHeight); y++) { for (int x = ox; x < System.Math.Min(ox + width, imageWidth); x++) { uint v = DecodeRGBA(pix[y * imageWidth + x]); #if PB_RENDER_PICKER_TEXTURE rectImg.Add(pix[y * imageWidth + x]); #endif if (used.Add(v) && map.TryGetValue(v, out hit)) { if (selected.TryGetValue(hit.item1, out indexes)) indexes.Add(hit.item2); else selected.Add(hit.item1, new HashSet() { hit.item2 }); } } } var coincidentSelection = new Dictionary>(); // workaround for picking vertices that share a position but are not shared foreach (var meshSelection in selected) { var positions = meshSelection.Key.positionsInternal; var sharedVertices = meshSelection.Key.sharedVerticesInternal; var positionHash = new HashSet(meshSelection.Value.Select(x => VectorHash.GetHashCode(positions[sharedVertices[x][0]]))); var collected = new HashSet(); for (int i = 0, c = sharedVertices.Length; i < c; i++) { var hash = VectorHash.GetHashCode(positions[sharedVertices[i][0]]); if (positionHash.Contains(hash)) collected.Add(i); } coincidentSelection.Add(meshSelection.Key, collected); } selected = coincidentSelection; #if PB_RENDER_PICKER_TEXTURE if (width > 0 && height > 0) { Texture2D img = new Texture2D(width, height); img.SetPixels(rectImg.ToArray()); img.Apply(); System.IO.File.WriteAllBytes("Assets/rect_" + s_RenderTextureFormat.ToString() + ".png", img.EncodeToPNG()); #if UNITY_EDITOR UnityEditor.AssetDatabase.Refresh(); #endif UObject.DestroyImmediate(img); } #endif return selected; } /// /// Selects the objects contained within a rect (rectangular selection). /// /// Use this camera to evaluate whether any edge object(s) are inside the `rect`. /// Rect is in GUI space, where 0,0 is the top left of the screen, and `width = cam.pixelWidth / pointsPerPixel`. /// The collection of objects to check for selectability. ProBuilder verifies whether these objects fall inside the `rect`. /// True to include mesh elements that are hidden; false otherwise /// Width of the texture. If not specified, ProBuilder uses auto sizing (matches the camera's texture width). /// Height of the texture. If not specified, ProBuilder uses auto sizing (matches the camera's texture height). /// A dictionary of ProBuilderMesh and Edge objects that are in the selection `rect`. public static Dictionary> PickEdgesInRect( Camera camera, Rect pickerRect, IList selection, bool doDepthTest, int renderTextureWidth = -1, int renderTextureHeight = -1) { var selected = new Dictionary>(); #if PB_RENDER_PICKER_TEXTURE List rectImg = new List(); #endif Dictionary> map; Texture2D tex = RenderSelectionPickerTexture(camera, selection, doDepthTest, out map, renderTextureWidth, renderTextureHeight); Color32[] pix = tex.GetPixels32(); #if PB_RENDER_PICKER_TEXTURE System.IO.File.WriteAllBytes("Assets/edge_scene.png", tex.EncodeToPNG()); #endif int ox = System.Math.Max(0, Mathf.FloorToInt(pickerRect.x)); int oy = System.Math.Max(0, Mathf.FloorToInt((tex.height - pickerRect.y) - pickerRect.height)); int imageWidth = tex.width; int imageHeight = tex.height; int width = Mathf.FloorToInt(pickerRect.width); int height = Mathf.FloorToInt(pickerRect.height); UObject.DestroyImmediate(tex); var pixelCount = new Dictionary(); for (int y = oy; y < System.Math.Min(oy + height, imageHeight); y++) { for (int x = ox; x < System.Math.Min(ox + width, imageWidth); x++) { #if PB_RENDER_PICKER_TEXTURE rectImg.Add(pix[y * imageWidth + x]); #endif uint v = DecodeRGBA(pix[y * imageWidth + x]); if (v == k_PickerHashNone || v == k_PickerHashMax) continue; if (!pixelCount.ContainsKey(v)) pixelCount.Add(v, 1); else pixelCount[v] = pixelCount[v] + 1; } } foreach (var kvp in pixelCount) { SimpleTuple hit; if (kvp.Value > k_MinEdgePixelsForValidSelection && map.TryGetValue(kvp.Key, out hit)) { HashSet edges = null; if (selected.TryGetValue(hit.item1, out edges)) edges.Add(hit.item2); else selected.Add(hit.item1, new HashSet() {hit.item2}); } } #if PB_RENDER_PICKER_TEXTURE if (width > 0 && height > 0) { Texture2D img = new Texture2D(width, height); img.SetPixels(rectImg.ToArray()); img.Apply(); System.IO.File.WriteAllBytes("Assets/edge_rect_" + s_RenderTextureFormat.ToString() + ".png", img.EncodeToPNG()); #if UNITY_EDITOR UnityEditor.AssetDatabase.Refresh(); #endif UObject.DestroyImmediate(img); } #endif return selected; } /// /// Render the pb_Object selection with the special selection picker shader and return a texture and color -> {object, face} dictionary. /// /// /// /// /// /// /// internal static Texture2D RenderSelectionPickerTexture( Camera camera, IList selection, out Dictionary> map, int width = -1, int height = -1) { var pickerObjects = GenerateFacePickingObjects(selection, out map); BuiltinMaterials.facePickerMaterial.SetColor(k_FacePickerOcclusionTintUniform, k_Whitef); Texture2D tex = pickerRenderer.RenderLookupTexture(camera, BuiltinMaterials.selectionPickerShader, "ProBuilderPicker", width, height); foreach (GameObject go in pickerObjects) { UObject.DestroyImmediate(go.GetComponent().sharedMesh); UObject.DestroyImmediate(go); } return tex; } /// /// Render the pb_Object selection with the special selection picker shader and return a texture and color -> {object, sharedIndex} dictionary. /// /// /// /// /// /// /// /// internal static Texture2D RenderSelectionPickerTexture( Camera camera, IList selection, bool doDepthTest, out Dictionary> map, int width = -1, int height = -1) { GameObject[] depthObjects, pickerObjects; GenerateVertexPickingObjects(selection, doDepthTest, out map, out depthObjects, out pickerObjects); BuiltinMaterials.facePickerMaterial.SetColor(k_FacePickerOcclusionTintUniform, k_Blackf); Texture2D tex = pickerRenderer.RenderLookupTexture(camera, BuiltinMaterials.selectionPickerShader, "ProBuilderPicker", width, height); for (int i = 0, c = pickerObjects.Length; i < c; i++) { UObject.DestroyImmediate(pickerObjects[i].GetComponent().sharedMesh); UObject.DestroyImmediate(pickerObjects[i]); } if (doDepthTest) { for (int i = 0, c = depthObjects.Length; i < c; i++) { UObject.DestroyImmediate(depthObjects[i]); } } return tex; } /// /// Render the pb_Object selection with the special selection picker shader and return a texture and color -> {object, edge} dictionary. /// /// /// /// /// /// /// /// internal static Texture2D RenderSelectionPickerTexture( Camera camera, IList selection, bool doDepthTest, out Dictionary> map, int width = -1, int height = -1) { GameObject[] depthObjects, pickerObjects; GenerateEdgePickingObjects(selection, doDepthTest, out map, out depthObjects, out pickerObjects); BuiltinMaterials.facePickerMaterial.SetColor(k_FacePickerOcclusionTintUniform, k_Blackf); Texture2D tex = pickerRenderer.RenderLookupTexture(camera, BuiltinMaterials.selectionPickerShader, "ProBuilderPicker", width, height); for (int i = 0, c = pickerObjects.Length; i < c; i++) { UObject.DestroyImmediate(pickerObjects[i].GetComponent().sharedMesh); UObject.DestroyImmediate(pickerObjects[i]); } if (doDepthTest) { for (int i = 0, c = depthObjects.Length; i < c; i++) { UObject.DestroyImmediate(depthObjects[i]); } } return tex; } static GameObject[] GenerateFacePickingObjects( IList selection, out Dictionary> map) { int selectionCount = selection.Count; GameObject[] pickerObjects = new GameObject[selectionCount]; map = new Dictionary>(); uint index = 0; for (int i = 0; i < selectionCount; i++) { var pb = selection[i]; Mesh m = new Mesh(); m.vertices = pb.positionsInternal; m.triangles = pb.facesInternal.SelectMany(x => x.indexesInternal).ToArray(); Color32[] colors = new Color32[m.vertexCount]; foreach (Face f in pb.facesInternal) { Color32 color = EncodeRGBA(index++); map.Add(DecodeRGBA(color), new SimpleTuple(pb, f)); for (int n = 0; n < f.distinctIndexesInternal.Length; n++) colors[f.distinctIndexesInternal[n]] = color; } m.colors32 = colors; GameObject go = InternalUtility.MeshGameObjectWithTransform(pb.name + " (Face Depth Test)", pb.transform, m, BuiltinMaterials.facePickerMaterial, true); pickerObjects[i] = go; } return pickerObjects; } static void GenerateVertexPickingObjects( IList selection, bool doDepthTest, out Dictionary> map, out GameObject[] depthObjects, out GameObject[] pickerObjects) { map = new Dictionary>(); // don't start at 0 because that means one vertex would be black, matching // the color used to cull hidden vertices. uint index = 0x02; int selectionCount = selection.Count; pickerObjects = new GameObject[selectionCount]; for (int i = 0; i < selectionCount; i++) { // build vertex billboards var pb = selection[i]; var mesh = BuildVertexMesh(pb, map, ref index); GameObject go = InternalUtility.MeshGameObjectWithTransform(pb.name + " (Vertex Billboards)", pb.transform, mesh, BuiltinMaterials.vertexPickerMaterial, true); pickerObjects[i] = go; } if (doDepthTest) { depthObjects = new GameObject[selectionCount]; // copy the select gameobject just for z-write for (int i = 0; i < selectionCount; i++) { var pb = selection[i]; GameObject go = InternalUtility.MeshGameObjectWithTransform(pb.name + " (Depth Mask)", pb.transform, pb.mesh, BuiltinMaterials.facePickerMaterial, true); depthObjects[i] = go; } } else { depthObjects = null; } } static void GenerateEdgePickingObjects( IList selection, bool doDepthTest, out Dictionary> map, out GameObject[] depthObjects, out GameObject[] pickerObjects) { map = new Dictionary>(); uint index = 0x2; int selectionCount = selection.Count; pickerObjects = new GameObject[selectionCount]; for (int i = 0; i < selectionCount; i++) { // build edge billboards var pb = selection[i]; var mesh = BuildEdgeMesh(pb, map, ref index); GameObject go = InternalUtility.MeshGameObjectWithTransform(pb.name + " (Edge Billboards)", pb.transform, mesh, BuiltinMaterials.edgePickerMaterial, true); pickerObjects[i] = go; } if (doDepthTest) { depthObjects = new GameObject[selectionCount]; for (int i = 0; i < selectionCount; i++) { var pb = selection[i]; // copy the select gameobject just for z-write GameObject go = InternalUtility.MeshGameObjectWithTransform(pb.name + " (Depth Mask)", pb.transform, pb.mesh, BuiltinMaterials.facePickerMaterial, true); depthObjects[i] = go; } } else { depthObjects = null; } } static Mesh BuildVertexMesh(ProBuilderMesh pb, Dictionary> map, ref uint index) { int length = System.Math.Min(pb.sharedVerticesInternal.Length, ushort.MaxValue / 4 - 1); Vector3[] t_billboards = new Vector3[length * 4]; Vector2[] t_uvs = new Vector2[length * 4]; Vector2[] t_uv2 = new Vector2[length * 4]; Color[] t_col = new Color[length * 4]; int[] t_tris = new int[length * 6]; int n = 0; int t = 0; Vector3 up = Vector3.up; Vector3 right = Vector3.right; for (int i = 0; i < length; i++) { Vector3 v = pb.positionsInternal[pb.sharedVerticesInternal[i][0]]; t_billboards[t + 0] = v; t_billboards[t + 1] = v; t_billboards[t + 2] = v; t_billboards[t + 3] = v; t_uvs[t + 0] = Vector3.zero; t_uvs[t + 1] = Vector3.right; t_uvs[t + 2] = Vector3.up; t_uvs[t + 3] = Vector3.one; t_uv2[t + 0] = -up - right; t_uv2[t + 1] = -up + right; t_uv2[t + 2] = up - right; t_uv2[t + 3] = up + right; t_tris[n + 0] = t + 0; t_tris[n + 1] = t + 1; t_tris[n + 2] = t + 2; t_tris[n + 3] = t + 1; t_tris[n + 4] = t + 3; t_tris[n + 5] = t + 2; Color32 color = EncodeRGBA(index); map.Add(index++, new SimpleTuple(pb, i)); t_col[t + 0] = color; t_col[t + 1] = color; t_col[t + 2] = color; t_col[t + 3] = color; t += 4; n += 6; } Mesh mesh = new Mesh(); mesh.name = "Vertex Billboard"; mesh.vertices = t_billboards; mesh.uv = t_uvs; mesh.uv2 = t_uv2; mesh.colors = t_col; mesh.triangles = t_tris; return mesh; } static Mesh BuildEdgeMesh(ProBuilderMesh pb, Dictionary> map, ref uint index) { int edgeCount = 0; int faceCount = pb.faceCount; for (int i = 0; i < faceCount; i++) edgeCount += pb.facesInternal[i].edgesInternal.Length; int elementCount = System.Math.Min(edgeCount, ushort.MaxValue / 2 - 1); Vector3[] positions = new Vector3[elementCount * 2]; Color32[] color = new Color32[elementCount * 2]; int[] tris = new int[elementCount * 2]; int edgeIndex = 0; for (int i = 0; i < faceCount && edgeIndex < elementCount; i++) { for (int n = 0; n < pb.facesInternal[i].edgesInternal.Length && edgeIndex < elementCount; n++) { var edge = pb.facesInternal[i].edgesInternal[n]; Vector3 a = pb.positionsInternal[edge.a]; Vector3 b = pb.positionsInternal[edge.b]; int positionIndex = edgeIndex * 2; positions[positionIndex + 0] = a; positions[positionIndex + 1] = b; Color32 c = EncodeRGBA(index); map.Add(index++, new SimpleTuple(pb, edge)); color[positionIndex + 0] = c; color[positionIndex + 1] = c; tris[positionIndex + 0] = positionIndex + 0; tris[positionIndex + 1] = positionIndex + 1; edgeIndex++; } } Mesh mesh = new Mesh(); mesh.name = "Edge Billboard"; mesh.vertices = positions; mesh.colors32 = color; mesh.subMeshCount = 1; mesh.SetIndices(tris, MeshTopology.Lines, 0); return mesh; } /// /// Decodes Color32.RGB values to a 32-bit unsigned int, using the RGB as the little bytes. Discards the hi byte (alpha). /// /// The color to decode. /// 32-bit unsigned int containing the decoded RGB values. public static uint DecodeRGBA(Color32 color) { uint r = (uint)color.r; uint g = (uint)color.g; uint b = (uint)color.b; if (System.BitConverter.IsLittleEndian) return r << 16 | g << 8 | b; else return r << 24 | g << 16 | b << 8; } /// /// Encodes the low 24 bits of a 32-bit unsigned int to Color32.RGB values, using 255 for the alpha. /// /// /// Color32 containing the encoded values. public static Color32 EncodeRGBA(uint hash) { // skip using BitConverter.GetBytes since this is super simple // bit math, and allocating arrays for each conversion is expensive if (System.BitConverter.IsLittleEndian) return new Color32( (byte)(hash >> 16 & 0xFF), (byte)(hash >> 8 & 0xFF), (byte)(hash & 0xFF), (byte)(255)); else return new Color32( (byte)(hash >> 24 & 0xFF), (byte)(hash >> 16 & 0xFF), (byte)(hash >> 8 & 0xFF), (byte)(255)); } static bool ShouldUseHDRP() { #if HDRP_7_1_0_OR_NEWER return true; #else return false; #endif } } }