diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.FixedExtrude.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.FixedExtrude.cs index 966ea68..f132dd3 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.FixedExtrude.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.FixedExtrude.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -22,9 +22,18 @@ private void FixedExtrude_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -35,4 +44,4 @@ private void FixedExtrude_Rebuild() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.LinearStaircase.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.LinearStaircase.cs index db1a563..17ec049 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.LinearStaircase.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.LinearStaircase.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -33,9 +33,18 @@ private void LinearStaircase_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -46,4 +55,4 @@ private void LinearStaircase_Rebuild() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveChopped.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveChopped.cs index a6a5f4e..9291e05 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveChopped.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveChopped.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -35,9 +35,18 @@ private void RevolveChopped_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -48,4 +57,4 @@ private void RevolveChopped_Rebuild() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveExtrude.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveExtrude.cs index bbdf6e6..1a38b52 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveExtrude.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.RevolveExtrude.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -42,9 +42,18 @@ private void RevolveExtrude_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -55,4 +64,4 @@ private void RevolveExtrude_Rebuild() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.ScaledExtrude.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.ScaledExtrude.cs index 28b33f1..6796f95 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.ScaledExtrude.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.ScaledExtrude.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -30,9 +30,18 @@ private void ScaledExtrude_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -43,4 +52,4 @@ private void ScaledExtrude_Rebuild() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Editor/Scene/RealtimeCSGTarget.SplineExtrude.cs b/Scripts/Editor/Scene/RealtimeCSGTarget.SplineExtrude.cs index 86125ed..f90b0fc 100644 --- a/Scripts/Editor/Scene/RealtimeCSGTarget.SplineExtrude.cs +++ b/Scripts/Editor/Scene/RealtimeCSGTarget.SplineExtrude.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR using UnityEngine; @@ -30,9 +30,18 @@ private void SplineExtrude_Rebuild() for (int i = 0; i < polygonMeshesCount; i++) { var polygonMesh = polygonMeshes[i]; - var planes = polygonMesh.ToMaterialPlanes(); - var brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); + // try the direct PolygonMesh -> brush path first + var brush = ExternalRealtimeCSG.CreateBrushFromPolygonMesh(parent, "Shape Editor Brush", polygonMesh); + if (brush != null) + { + brush.transform.SetParent(parent, false); + continue; + } + + // fall back to plane-based brush creation + var planes = polygonMesh.ToMaterialPlanes(); + brush = ExternalRealtimeCSG.CreateBrushFromPlanes("Shape Editor Brush", planes.planes, GetMaterials(planes.materials), polygonMesh.booleanOperator); if (brush != null) brush.transform.SetParent(parent, false); } @@ -53,7 +62,7 @@ private MathEx.Spline3 GetSpline3() private Vector3[] GetLocalChildPoints() { int childCount = transform.childCount; - Vector3[] points = new Vector3[childCount]; + var points = new Vector3[childCount]; for (int i = 0; i < childCount; i++) points[i] = transform.GetChild(i).localPosition; return points; @@ -61,4 +70,4 @@ private Vector3[] GetLocalChildPoints() } } -#endif \ No newline at end of file +#endif diff --git a/Scripts/Utilities/ExternalRealtimeCSG.cs b/Scripts/Utilities/ExternalRealtimeCSG.cs index a984837..22274c3 100644 --- a/Scripts/Utilities/ExternalRealtimeCSG.cs +++ b/Scripts/Utilities/ExternalRealtimeCSG.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR // contains source code from https://github.com/LogicalError/realtime-CSG-for-unity (see Licenses/RealtimeCSG.txt). @@ -110,6 +110,51 @@ public class ExternalRealtimeCSG /// private static PropertyInfo materialUtilityWallMaterialProperty = null; + /// + /// The cached GeometryUtility type after initialization. + /// + private static Type geometryUtility = null; + + /// + /// The cached GeometryUtility.CalcPolygonPlane method after initialization. + /// + private static MethodInfo calcPolygonPlaneMethod = null; + + /// + /// The cached GeometryUtility.CalculateTangents method after initialization. + /// + private static MethodInfo calculateTangentsMethod = null; + + /// + /// The cached ControlMeshUtility type after initialization. + /// + private static Type controlMeshUtility = null; + + /// + /// The cached ControlMeshUtility.Validate method after initialization. + /// + private static MethodInfo validateControlMeshMethod = null; + + /// + /// The cached ShapeUtility.EnsureInitialized method after initialization. + /// + private static MethodInfo ensureInitializedShapeMethod = null; + + /// + /// The cached CSGSettings.DefaultMaterial property after initialization. + /// + private static PropertyInfo csgSettingsDefaultMaterialProperty = null; + + /// + /// The cached CSGSettings.DefaultTexGenFlags property after initialization. + /// + private static PropertyInfo csgSettingsDefaultTexGenFlagsProperty = null; + + /// + /// The cached MathConstants.oneVector3 field after initialization. + /// + private static FieldInfo oneVector3Field = null; + /// /// Used to store whether an initialization error occured. /// @@ -189,6 +234,42 @@ public static bool IsAvailable() materialUtilityWallMaterialProperty = materialUtility.GetProperty("WallMaterial"); if (materialUtilityWallMaterialProperty == null) { initializationError = true; return false; } + geometryUtility = GetType("RealtimeCSG.GeometryUtility"); + if (geometryUtility == null) { initializationError = true; return false; } + + calcPolygonPlaneMethod = geometryUtility.GetMethodByName("CalcPolygonPlane", "controlMesh", "polygonIndex"); + if (calcPolygonPlaneMethod == null) { initializationError = true; return false; } + + calculateTangentsMethod = geometryUtility.GetMethodByName("CalculateTangents", "normal", out _, out _); + if (calculateTangentsMethod == null) { initializationError = true; return false; } + + controlMeshUtility = GetType("RealtimeCSG.ControlMeshUtility"); + if (controlMeshUtility == null) { initializationError = true; return false; } + + validateControlMeshMethod = controlMeshUtility.GetMethodByName("Validate", "controlMesh", "shape"); + if (validateControlMeshMethod == null) { initializationError = true; return false; } + + var shapeUtility = GetType("RealtimeCSG.ShapeUtility"); + if (shapeUtility == null) { initializationError = true; return false; } + + ensureInitializedShapeMethod = shapeUtility.GetMethodByName("EnsureInitialized", "shape"); + if (ensureInitializedShapeMethod == null) { initializationError = true; return false; } + + var csgSettings = GetType("RealtimeCSG.CSGSettings"); + if (csgSettings == null) { initializationError = true; return false; } + + csgSettingsDefaultMaterialProperty = csgSettings.GetProperty("DefaultMaterial"); + if (csgSettingsDefaultMaterialProperty == null) { initializationError = true; return false; } + + csgSettingsDefaultTexGenFlagsProperty = csgSettings.GetProperty("DefaultTexGenFlags"); + if (csgSettingsDefaultTexGenFlagsProperty == null) { initializationError = true; return false; } + + var mathConstants = GetType("RealtimeCSG.MathConstants"); + if (mathConstants == null) { initializationError = true; return false; } + + oneVector3Field = mathConstants.GetField("oneVector3"); + if (oneVector3Field == null) { initializationError = true; return false; } + initializationSuccess = true; return true; } @@ -233,7 +314,7 @@ public static Material WallMaterial get { if (!IsAvailable()) return null; - return (Material)materialUtilityWallMaterialProperty.GetValue(materialUtility); + return (Material)csgSettingsDefaultMaterialProperty.GetValue(null); } } @@ -286,6 +367,188 @@ public static MonoBehaviour CreateBrushFromPlanes(string brushName, Plane[] plan return (MonoBehaviour)brush; } + /// + /// Creates a RealtimeCSG brush directly from a PolygonMesh by building the half-edge + /// connectivity data (ControlMesh) and matching Shape/Surface/TexGen data, bypassing the + /// plane-intersection approach which can fail on certain mesh topologies. + /// + /// The parent transform for the created brush. + /// The name of the brush. + /// The PolygonMesh to convert. + /// A CSGBrush component on success, or null on failure. + public static MonoBehaviour CreateBrushFromPolygonMesh(Transform parent, string brushName, PolygonMesh mesh) + { + if (!IsAvailable()) return null; + + // skip degenerate meshes + if (mesh == null || mesh.Count == 0) return null; + + // build unique vertex list preserving insertion order + var vertexSet = new HashSet(); + var uniqueVertices = new List(); + foreach (var polygon in mesh) + { + foreach (var vertex in polygon) + { + if (vertexSet.Add(vertex.position)) + uniqueVertices.Add(vertex.position); + } + } + + // build half-edge index arrays: one edge per polygon vertex + var polygonCount = mesh.Count; + var edgeIndices = new List(); + foreach (var polygon in mesh) + { + for (int j = 0; j < polygon.Count; j++) + edgeIndices.Add(0); // placeholder, filled below + } + + // build half-edges with polygon/vertex indices (twin set to -1 for now) + var halfEdges = new List(); + foreach (var polygon in mesh) + { + for (int j = 0; j < polygon.Count; j++) + { + var vertexIndex = uniqueVertices.FindIndex(v => v.EqualsWithEpsilon5(polygon[j].position)); + halfEdges.Add(CreateHalfEdge(polygon.Count, j, vertexIndex)); + } + } + + // associate twin edges by finding shared polygon boundaries + var edgeCount = halfEdges.Count; + for (int i = 0; i < edgeCount; i++) + { + var hei = GetHalfEdgePolygonIndex(halfEdges[i]); + var hej = GetHalfEdgeVertexIndex(halfEdges[i]); + var polyI = mesh[hei]; + var prevInPolyI = polyI.PreviousVertex(hej); + var prevPosI = prevInPolyI.position; + + for (int k = 0; k < edgeCount; k++) + { + if (k == i) continue; + var hek_poly = GetHalfEdgePolygonIndex(halfEdges[k]); + var hek_vi = GetHalfEdgeVertexIndex(halfEdges[k]); + var polyK = mesh[hek_poly]; + var currInPolyK = polyK[hek_vi]; + var prevInPolyK = polyK.PreviousVertex(hek_vi); + + if (prevPosI.EqualsWithEpsilon5(currInPolyK.position) && + prevInPolyK.position.EqualsWithEpsilon5(polyI[hej].position)) + { + SetHalfEdgeTwinIndex(halfEdges[i], k); + break; + } + } + } + + // build the ControlMesh + var controlMeshType = GetType("RealtimeCSG.Legacy.ControlMesh"); + if (controlMeshType == null) return null; + + var controlMesh = Activator.CreateInstance(controlMeshType); + SetField(controlMesh, "Polygons", mesh.Count); + SetField(controlMesh, "Vertices", uniqueVertices.ToArray()); + SetField(controlMesh, "Edges", halfEdges.ToArray()); + SetField(controlMesh, "Valid", true); + + // build the Shape with per-polygon surfaces + var shapeType = GetType("RealtimeCSG.Legacy.Shape"); + if (shapeType == null) return null; + + var shape = Activator.CreateInstance(shapeType); + var surfaces = new object[polygonCount]; + var texGens = new object[polygonCount]; + var texGenFlags = new object[polygonCount]; + var defaultMaterial = WallMaterial ?? UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Diffuse.mat"); + var oneVector3 = (Vector3)oneVector3Field.GetValue(null); + var defaultTexGenFlags = csgSettingsDefaultTexGenFlagsProperty.GetValue(null); + + for (int i = 0; i < polygonCount; i++) + { + // create Surface + var surfaceType = GetType("RealtimeCSG.Legacy.Surface"); + var surface = Activator.CreateInstance(surfaceType); + var plane = (Plane)calcPolygonPlaneMethod.Invoke(null, new object[] { controlMesh, (short)i }); + SetField(surface, "Plane", plane); + var tangentOut = Vector3.zero; + var binormalOut = Vector3.zero; + calculateTangentsMethod.Invoke(null, new object[] { plane.normal, tangentOut, binormalOut }); + SetField(surface, "Tangent", tangentOut); + SetField(surface, "BiNormal", binormalOut); + surfaces[i] = surface; + + // create TexGen + var texGenType = GetType("RealtimeCSG.Legacy.TexGen"); + var texGen = Activator.CreateInstance(texGenType); + SetField(texGen, "RenderMaterial", defaultMaterial); + SetField(texGen, "Scale", oneVector3); + texGens[i] = texGen; + + texGenFlags[i] = defaultTexGenFlags; + } + + SetField(shape, "Surfaces", surfaces); + SetField(shape, "TexGens", texGens); + SetField(shape, "TexGenFlags", texGenFlags); + + // validate the control mesh + ensureInitializedShapeMethod.Invoke(null, new object[] { shape }); + var isValid = (bool)validateControlMeshMethod.Invoke(null, new object[] { controlMesh, shape }); + SetField(controlMesh, "Valid", isValid); + + if (!isValid) + { + Debug.LogWarning($"[ShapeEditor] CreateBrushFromPolygonMesh: control mesh validation failed for '{brushName}', falling back to plane-based approach."); + return null; + } + + // create the brush + var brush = (MonoBehaviour)createBrushMethod.Invoke(null, new object[] { parent, brushName, controlMesh, shape }); + if (brush != null) + Debug.Log($"[ShapeEditor] CreateBrushFromPolygonMesh: successfully created brush '{brushName}' with {polygonCount} polygons and {uniqueVertices.Count} vertices."); + + return brush; + } + + private static object CreateHalfEdge(int polygonIndex, int vertexIndex, int twinIndex) + { + var halfEdgeType = GetType("RealtimeCSG.Legacy.HalfEdge"); + if (halfEdgeType == null) return null; + var he = Activator.CreateInstance(halfEdgeType); + SetField(he, "PolygonIndex", (short)polygonIndex); + SetField(he, "VertexIndex", (short)vertexIndex); + SetField(he, "TwinIndex", (short)twinIndex); + return he; + } + + private static short GetHalfEdgePolygonIndex(object halfEdge) + { + var t = halfEdge.GetType(); + return (short)t.GetField("PolygonIndex").GetValue(halfEdge); + } + + private static short GetHalfEdgeVertexIndex(object halfEdge) + { + var t = halfEdge.GetType(); + return (short)t.GetField("VertexIndex").GetValue(halfEdge); + } + + private static void SetHalfEdgeTwinIndex(object halfEdge, int twinIndex) + { + var t = halfEdge.GetType(); + t.GetField("TwinIndex").SetValue(halfEdge, (short)twinIndex); + } + + private static void SetField(object obj, string fieldName, object value) + { + var t = obj.GetType(); + var field = t.GetField(fieldName); + if (field != null) + field.SetValue(obj, value); + } + private static void DestroyCSGNodeComponent(GameObject gameObject) { var component = gameObject.GetComponent(csgNode); @@ -325,4 +588,4 @@ public static void UpdateSelection() } } -#endif \ No newline at end of file +#endif