├── Editor.meta ├── Editor ├── PipeEditor.cs ├── PipeEditor.cs.meta ├── leth.instantpipes.Editor.asmdef └── leth.instantpipes.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── PathCreator.cs ├── PathCreator.cs.meta ├── Pipe.cs ├── Pipe.cs.meta ├── PipeGenerator.cs ├── PipeGenerator.cs.meta ├── leth.instantpipes.asmdef └── leth.instantpipes.asmdef.meta ├── package.json └── package.json.meta /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8908e68b2bbd2e9498fbbab048c25144 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/PipeEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEditor; 4 | 5 | namespace InstantPipes 6 | { 7 | [CustomEditor(typeof(PipeGenerator))] 8 | public class PipeEditor : Editor 9 | { 10 | private PipeGenerator _generator; 11 | 12 | private static bool _autoRegenerate = true; 13 | private static bool _previewPath = false; 14 | private static bool _settingsFoldoutActive = true; 15 | private static int _editingMode = 0; 16 | 17 | private Vector3 _startDragPoint; 18 | private Vector3 _startDragNormal; 19 | private bool _isDragging = false; 20 | private bool _lastBuildFailed = false; 21 | 22 | private int _selectedPipeIndex = -1; 23 | private List _selectedPointsIndexes = new List(); 24 | private Vector3 _positionHandle; 25 | 26 | private void OnEnable() 27 | { 28 | _generator = (PipeGenerator)target; 29 | Undo.undoRedoPerformed += OnUndo; 30 | Tools.hidden = true; 31 | } 32 | 33 | private void OnDisable() 34 | { 35 | Undo.undoRedoPerformed -= OnUndo; 36 | Tools.hidden = false; 37 | } 38 | 39 | private void OnUndo() 40 | { 41 | _generator.UpdateMesh(); 42 | SetHandlePosition(); 43 | } 44 | 45 | public override void OnInspectorGUI() 46 | { 47 | if (_generator.transform.position != Vector3.zero) 48 | { 49 | EditorGUILayout.Space(5); 50 | EditorGUILayout.HelpBox("The Generator position should be set to (0,0,0)!", MessageType.Warning); 51 | var buttonRect = GUILayoutUtility.GetLastRect(); 52 | buttonRect.x = buttonRect.width + buttonRect.x - 37; 53 | buttonRect.width = 30; 54 | buttonRect.y = buttonRect.y + 7; 55 | buttonRect.height = 24; 56 | if (GUI.Button(buttonRect, "Fix")) _generator.transform.position = Vector3.zero; 57 | } 58 | 59 | EditorGUILayout.Space(5); 60 | _editingMode = GUILayout.Toolbar(_editingMode, new string[] { "Create", "Edit" }); 61 | EditorGUILayout.Space(5); 62 | 63 | if (_editingMode == 0) PathGUI(); 64 | if (_editingMode == 1) EditGUI(); 65 | 66 | EditorGUILayout.Space(5); 67 | 68 | _settingsFoldoutActive = EditorGUILayout.BeginFoldoutHeaderGroup(_settingsFoldoutActive, "Settings"); 69 | if (_settingsFoldoutActive) SettingsGUI(); 70 | EditorGUILayout.EndFoldoutHeaderGroup(); 71 | } 72 | 73 | private void SettingsGUI() 74 | { 75 | EditorGUI.BeginChangeCheck(); 76 | var radius = EditorGUILayout.FloatField("Radius", _generator.Radius); 77 | var curvature = EditorGUILayout.Slider("Curvature", _generator.Curvature, 0.01f, _generator.MaxCurvature); 78 | var material = (Material)EditorGUILayout.ObjectField("Material", _generator.Material, typeof(Material), false); 79 | 80 | GUILayout.BeginHorizontal(); 81 | var isSeparateRingsMaterial = EditorGUILayout.ToggleLeft("Ring Material", _generator.IsSeparateRingsMaterial, GUILayout.Width(EditorGUIUtility.labelWidth)); 82 | EditorGUI.BeginDisabledGroup(!isSeparateRingsMaterial); 83 | var ringMaterial = (Material)EditorGUILayout.ObjectField(_generator.RingMaterial, typeof(Material), false); 84 | EditorGUI.EndDisabledGroup(); 85 | GUILayout.EndHorizontal(); 86 | var ringsUVScale = EditorGUILayout.FloatField("Rings UV Scale", _generator.RingsUVScale); 87 | EditorGUILayout.Space(10); 88 | 89 | var hasRings = EditorGUILayout.ToggleLeft("Rings", _generator.HasRings, EditorStyles.boldLabel); 90 | float ringRadius = _generator.RingRadius, ringThickness = _generator.RingThickness; 91 | bool hasExtrusion = _generator.HasExtrusion; 92 | if (_generator.HasRings) 93 | { 94 | hasExtrusion = EditorGUILayout.Toggle("Extrusion", _generator.HasExtrusion); 95 | ringRadius = EditorGUILayout.Slider("Radius", _generator.RingRadius, 0, radius); 96 | EditorGUI.BeginDisabledGroup(hasExtrusion); 97 | ringThickness = EditorGUILayout.Slider("Thickness", _generator.RingThickness, 0, radius); 98 | EditorGUI.EndDisabledGroup(); 99 | EditorGUILayout.Space(10); 100 | } 101 | 102 | var hasCaps = EditorGUILayout.ToggleLeft("End Caps", _generator.HasCaps, EditorStyles.boldLabel); 103 | var maxEndCapOffset = _generator.PathCreator.Height - _generator.CapThickness - _generator.Radius; 104 | float capRadius = _generator.CapRadius, capThickness = _generator.CapThickness, capOffset = _generator.CapOffset; 105 | if (_generator.HasCaps) 106 | { 107 | capRadius = EditorGUILayout.Slider("Radius", _generator.CapRadius, 0, radius); 108 | capThickness = EditorGUILayout.Slider("Thickness", _generator.CapThickness, 0, radius); 109 | capOffset = EditorGUILayout.Slider("Offset", _generator.CapOffset, 0, maxEndCapOffset); 110 | } 111 | EditorGUILayout.Space(10); 112 | 113 | GUILayout.Label("Quality", EditorStyles.boldLabel); 114 | var edgeCount = EditorGUILayout.IntSlider("Edges", _generator.EdgeCount, 3, 40); 115 | var segmentCount = EditorGUILayout.IntSlider("Segments", _generator.CurvedSegmentCount, 1, 40); 116 | 117 | if (EditorGUI.EndChangeCheck()) 118 | { 119 | Undo.RecordObject(_generator, "Set Field Value"); 120 | 121 | _generator.Material = material; 122 | _generator.IsSeparateRingsMaterial = isSeparateRingsMaterial; 123 | _generator.RingMaterial = ringMaterial; 124 | _generator.RingsUVScale = Mathf.Max(0.01f, ringsUVScale); 125 | _generator.Radius = Mathf.Max(0.01f, radius); 126 | _generator.Curvature = Mathf.Clamp(curvature, 0.01f, _generator.MaxCurvature); 127 | _generator.HasRings = hasRings; 128 | _generator.HasExtrusion = hasExtrusion; 129 | _generator.RingRadius = Mathf.Clamp(ringRadius, 0, radius); 130 | _generator.RingThickness = Mathf.Clamp(ringThickness, 0, radius); 131 | _generator.HasCaps = hasCaps; 132 | _generator.CapRadius = Mathf.Clamp(capRadius, 0, radius); 133 | _generator.CapThickness = Mathf.Clamp(capThickness, 0, radius); 134 | _generator.CapOffset = Mathf.Clamp(capOffset, 0, maxEndCapOffset); 135 | _generator.EdgeCount = edgeCount; 136 | _generator.CurvedSegmentCount = segmentCount; 137 | 138 | _generator.PathCreator.Radius = radius + (hasRings ? ringRadius : 0); 139 | _generator.UpdateMesh(); 140 | } 141 | } 142 | 143 | private void EditGUI() 144 | { 145 | _selectedPointsIndexes.Sort(); 146 | if (_selectedPipeIndex != -1 && _selectedPointsIndexes.Count != 0) 147 | { 148 | EditorGUI.BeginChangeCheck(); 149 | var positions = new Vector3[_generator.Pipes[_selectedPipeIndex].Points.Count]; 150 | foreach (var index in _selectedPointsIndexes) 151 | { 152 | positions[index] = DrawVectorField($"{index}", _generator.Pipes[_selectedPipeIndex].Points[index]); 153 | } 154 | if (EditorGUI.EndChangeCheck()) 155 | { 156 | Undo.RecordObject(_generator, "Moved a point"); 157 | foreach (var index in _selectedPointsIndexes) 158 | { 159 | _generator.Pipes[_selectedPipeIndex].Points[index] = positions[index]; 160 | } 161 | _generator.UpdateMesh(); 162 | SetHandlePosition(); 163 | } 164 | GUILayout.Space(10); 165 | } 166 | 167 | GUI.enabled = _selectedPipeIndex != -1; 168 | if (GUILayout.Button("Delete Selected Pipe")) 169 | { 170 | Undo.RecordObject(_generator, "Deleted a Pipe"); 171 | _generator.RemovePipe(_selectedPipeIndex); 172 | _selectedPipeIndex = -1; 173 | } 174 | GUI.enabled = true; 175 | 176 | GUI.enabled = _selectedPipeIndex != -1 && _selectedPointsIndexes.Count != 0 && _generator.Pipes[_selectedPipeIndex].Points.Count - _selectedPointsIndexes.Count >= 2; 177 | if (GUILayout.Button(_selectedPointsIndexes.Count == 1 ? "Delete Selected Point" : "Delete Selected Points")) 178 | { 179 | Undo.RecordObject(_generator, "Deleted a point"); 180 | _selectedPointsIndexes.Reverse(); 181 | _selectedPointsIndexes.ForEach(pointIndex => _generator.RemovePoint(_selectedPipeIndex, pointIndex)); 182 | _selectedPipeIndex = -1; 183 | _selectedPointsIndexes.Clear(); 184 | } 185 | GUI.enabled = true; 186 | 187 | GUI.enabled = _selectedPipeIndex != -1 && _selectedPointsIndexes.Count == 1; 188 | if (GUILayout.Button("Insert a point")) 189 | { 190 | Undo.RecordObject(_generator, "Inserted a point"); 191 | _generator.InsertPoint(_selectedPipeIndex, _selectedPointsIndexes[0]); 192 | _selectedPointsIndexes[0]++; 193 | Repaint(); 194 | } 195 | GUI.enabled = true; 196 | 197 | if (GUILayout.Button("Erase Everything")) 198 | { 199 | Undo.RecordObject(_generator, "Erased all Pipes"); 200 | _generator.Pipes.Clear(); 201 | _generator.UpdateMesh(); 202 | } 203 | } 204 | 205 | private Vector3 DrawVectorField(string label, Vector3 value) 206 | { 207 | var defaultLabelWidth = EditorGUIUtility.labelWidth; 208 | EditorGUIUtility.labelWidth = 12; 209 | var vector = new Vector3(); 210 | 211 | GUILayout.BeginHorizontal(); 212 | EditorGUILayout.LabelField(label, GUILayout.Width(30)); 213 | GUILayout.Space(10); 214 | vector.x = EditorGUILayout.FloatField("X", value.x); 215 | vector.y = EditorGUILayout.FloatField("Y", value.y); 216 | vector.z = EditorGUILayout.FloatField("Z", value.z); 217 | GUILayout.EndHorizontal(); 218 | 219 | EditorGUIUtility.labelWidth = defaultLabelWidth; 220 | return vector; 221 | } 222 | 223 | private void PathGUI() 224 | { 225 | _previewPath = EditorGUILayout.Toggle("Preview Path", _previewPath); 226 | _generator.PipesAmount = EditorGUILayout.IntSlider("Amount", _generator.PipesAmount, 1, 10); 227 | 228 | EditorGUI.BeginChangeCheck(); 229 | 230 | var maxIterations = EditorGUILayout.IntField("Max Iterations", _generator.PathCreator.MaxIterations); 231 | var pathGridSize = EditorGUILayout.FloatField("Grid Size", _generator.PathCreator.GridSize); 232 | var gridRotation = EditorGUILayout.FloatField("Grid Y Angle", _generator.PathCreator.GridRotationY); 233 | var pathHeight = EditorGUILayout.FloatField("Height", _generator.PathCreator.Height); 234 | var chaos = EditorGUILayout.Slider("Chaos", _generator.PathCreator.Chaos, 0, 100); 235 | var straightPriority = EditorGUILayout.Slider("Straight Priority", _generator.PathCreator.StraightPathPriority, 0, 100); 236 | var nearObstaclePriority = EditorGUILayout.Slider("Near Obstacle Priority", _generator.PathCreator.NearObstaclesPriority, 0, 100); 237 | 238 | if (EditorGUI.EndChangeCheck()) 239 | { 240 | Undo.RecordObject(_generator, "Set Field Value"); 241 | 242 | _generator.PathCreator.GridSize = Mathf.Clamp(pathGridSize, _generator.PathCreator.Radius, Mathf.Infinity); 243 | _generator.PathCreator.GridRotationY = gridRotation; 244 | _generator.PathCreator.Height = Mathf.Clamp(pathHeight, pathGridSize, Mathf.Infinity); 245 | _generator.PathCreator.StraightPathPriority = straightPriority; 246 | _generator.PathCreator.NearObstaclesPriority = nearObstaclePriority; 247 | _generator.PathCreator.Chaos = chaos; 248 | _generator.PathCreator.MaxIterations = maxIterations; 249 | 250 | if (_autoRegenerate) RegeneratePaths(); 251 | } 252 | 253 | EditorGUILayout.BeginHorizontal(); 254 | if (GUILayout.Button("Regenerate")) RegeneratePaths(); 255 | _autoRegenerate = GUILayout.Toggle(_autoRegenerate, "Auto", GUILayout.Width(50)); 256 | EditorGUILayout.EndHorizontal(); 257 | 258 | if (_lastBuildFailed) 259 | { 260 | EditorGUILayout.HelpBox("Some paths weren't found!", MessageType.Warning); 261 | } 262 | } 263 | 264 | private void RegeneratePaths() 265 | { 266 | _lastBuildFailed = !_generator.RegeneratePaths(); 267 | } 268 | 269 | private void OnSceneGUI() 270 | { 271 | if (_editingMode == 0) 272 | HandlePathInput(Event.current); 273 | if (_editingMode == 1) 274 | HandleEdit(Event.current); 275 | } 276 | 277 | private void SetHandlePosition() 278 | { 279 | if (_selectedPointsIndexes.Count == 0) return; 280 | var sum = Vector3.zero; 281 | _selectedPointsIndexes.ForEach(index => sum += _generator.Pipes[_selectedPipeIndex].Points[index]); 282 | _positionHandle = sum / _selectedPointsIndexes.Count; 283 | } 284 | 285 | private void HandleEdit(Event evt) 286 | { 287 | Handles.matrix = _generator.transform.localToWorldMatrix; 288 | 289 | for (int i = 0; i < _generator.Pipes.Count; i++) 290 | { 291 | for (int j = 0; j < _generator.Pipes[i].Points.Count; j++) 292 | { 293 | Handles.color = (_selectedPipeIndex == i) ? Color.white : new Color(1, 1, 1, 0.4f); 294 | if (j != 0) 295 | { 296 | Handles.DrawLine(_generator.Pipes[i].Points[j], _generator.Pipes[i].Points[j - 1]); 297 | } 298 | 299 | if (_selectedPipeIndex == i && _selectedPointsIndexes.Contains(j)) Handles.color = Color.yellow; 300 | if (Handles.Button(_generator.Pipes[i].Points[j], Quaternion.identity, _generator.Radius * 1.5f, _generator.Radius * 2f, Handles.SphereHandleCap)) 301 | { 302 | if (evt.shift && _selectedPipeIndex == i) 303 | { 304 | if (!_selectedPointsIndexes.Contains(j)) 305 | _selectedPointsIndexes.Add(j); 306 | else 307 | _selectedPointsIndexes.Remove(j); 308 | } 309 | else 310 | { 311 | _selectedPointsIndexes = new List { j }; 312 | } 313 | 314 | _selectedPipeIndex = i; 315 | SetHandlePosition(); 316 | Repaint(); 317 | } 318 | } 319 | } 320 | 321 | if (_selectedPointsIndexes.Count != 0) 322 | { 323 | Handles.zTest = UnityEngine.Rendering.CompareFunction.Always; 324 | 325 | EditorGUI.BeginChangeCheck(); 326 | var position = Handles.PositionHandle(_positionHandle, Quaternion.identity); 327 | if (EditorGUI.EndChangeCheck()) 328 | { 329 | Undo.RecordObject(_generator, "Moved a point"); 330 | foreach (var index in _selectedPointsIndexes) 331 | { 332 | _generator.Pipes[_selectedPipeIndex].Points[index] += position - _positionHandle; 333 | } 334 | _positionHandle = position; 335 | _generator.UpdateMesh(); 336 | } 337 | 338 | if (evt.keyCode == KeyCode.A && !evt.control && Tools.viewTool != ViewTool.FPS) 339 | { 340 | _selectedPointsIndexes.Clear(); 341 | for (int i = 0; i < _generator.Pipes[_selectedPipeIndex].Points.Count; i++) 342 | { 343 | _selectedPointsIndexes.Add(i); 344 | Repaint(); 345 | } 346 | } 347 | } 348 | } 349 | 350 | private void HandlePathInput(Event evt) 351 | { 352 | var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition); 353 | var mouseHit = new RaycastHit(); 354 | 355 | if (Physics.Raycast(ray, out var hit, 1000)) 356 | { 357 | mouseHit = hit; 358 | } 359 | 360 | if (_isDragging) 361 | { 362 | bool canStartAtPoint = !Physics.SphereCast(new Ray(_startDragPoint, _startDragNormal), _generator.PathCreator.Radius, _generator.PathCreator.Height); 363 | bool canEndAtPoint = !Physics.SphereCast(new Ray(mouseHit.point, mouseHit.normal), _generator.PathCreator.Radius, _generator.PathCreator.Height); 364 | Handles.color = (canStartAtPoint && canEndAtPoint) ? Color.blue : Color.red; 365 | 366 | if (_previewPath) 367 | { 368 | var vectorLength = _generator.PipesAmount * _generator.Radius + (_generator.PipesAmount - 1) * _generator.PathCreator.GridSize; 369 | var startVector = Vector3.Cross(_startDragNormal, mouseHit.point - _startDragPoint).normalized * vectorLength; 370 | var endVector = Vector3.Cross(mouseHit.normal, mouseHit.point - _startDragPoint).normalized * vectorLength; 371 | var stepSize = startVector.magnitude / (_generator.PipesAmount); 372 | for (int i = 0; i < _generator.PipesAmount; i++) 373 | { 374 | var start = startVector.normalized * (stepSize * (i - _generator.PipesAmount / 2f + 0.5f)); 375 | var end = endVector.normalized * (stepSize * (i - _generator.PipesAmount / 2f + 0.5f)); 376 | var points = _generator.PathCreator.Create(_startDragPoint + start, _startDragNormal, mouseHit.point + end, mouseHit.normal); 377 | for (int j = 1; j < points.Count; j++) 378 | { 379 | #if UNITY_2021_3_OR_NEWER 380 | Handles.DrawLine(points[j], points[j - 1], 10); 381 | #else 382 | Handles.DrawLine(points[j], points[j - 1]); 383 | #endif 384 | } 385 | } 386 | } 387 | else 388 | { 389 | #if UNITY_2021_3_OR_NEWER 390 | Handles.DrawLine(_startDragPoint, mouseHit.point, 2); 391 | #else 392 | Handles.DrawLine(_startDragPoint, mouseHit.point); 393 | #endif 394 | } 395 | } 396 | 397 | if (evt.type == EventType.Layout) 398 | { 399 | HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive)); 400 | return; 401 | } 402 | 403 | if (evt.type == EventType.MouseDown && evt.button == 0 && evt.modifiers == EventModifiers.None) 404 | { 405 | _isDragging = true; 406 | _startDragNormal = mouseHit.normal; 407 | _startDragPoint = mouseHit.point; 408 | } 409 | 410 | if (_isDragging && evt.type == EventType.MouseUp && evt.button == 0 && evt.modifiers == EventModifiers.None) 411 | { 412 | _isDragging = false; 413 | 414 | if (Vector3.Distance(mouseHit.point, _startDragPoint) > _generator.PathCreator.GridSize) 415 | { 416 | Undo.RecordObject(_generator, "Add Pipe"); 417 | _lastBuildFailed = !_generator.AddPipe(_startDragPoint, _startDragNormal, mouseHit.point, mouseHit.normal); 418 | } 419 | } 420 | Repaint(); 421 | } 422 | } 423 | } -------------------------------------------------------------------------------- /Editor/PipeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8bb140e2c4bba8642b52f807ba5c8f5d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/leth.instantpipes.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leth.instantpipes.Editor", 3 | "rootNamespace": "InstantPipes", 4 | "references": [ 5 | "GUID:507c31c94ab77a54fb5ef52f553b2f5a" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /Editor/leth.instantpipes.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0bcff17482f21cb4ba43399beda4eb9b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eugene Radaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f6ed32f9cd5dd804ca3d41a69faf360c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instant Pipes 2 | 3 | An editor tool for procedurally generating pipes by just dragging the cursor from start to end — the pipe will find the path in a customizable way. 4 | 5 | ![Unity_3wtlMTU9I1](https://github.com/letharqic/InstantPipes/assets/44412176/912f3879-1d82-4408-8cef-2698b82608a0) 6 | 7 | ## Compatibility 8 | 9 | Unity 2019.4 or higher. 10 | 11 | ## Installation 12 | 13 | Add the package to your project via the [Package Manager](https://docs.unity3d.com/Manual/upm-ui.html) using the Git URL 14 | `https://github.com/leth4/InstantPipes.git`. You can also clone the repository and point the Package Manager to your local copy. 15 | 16 | ## Usage 17 | 18 | ### Starting out 19 | 20 | 1. Create an empty GameObject and set its world position to zero. 21 | 2. Add a `Pipe Generator` component. 22 | 3. Select a material for the `Material` property. 23 | 24 | If you're facing problems, visit the troubleshooting section. 25 | 26 | Ctrl+Z works with all actions. When you're commited to the pipes, you can just remove the component, the mesh will stay. 27 | 28 | ### Pipes Settings 29 | 30 | - `Curvature` changes the length of the curved parts, making pipes appear more or less curvy. Note that it applies after pathfinding, so in some cases high curvature value can make pipes intersect. 31 | - `Edges` property selects how many edges the pipes will have, and `Segments` is the amount of subdivisions in curved parts. 32 | - You can toggle `Rings` and `End Caps` and separately set up their radius and thickness. 33 | - Additionally, you can toggle `Extrusion` under the `Rings` label, replacing rings with extruded curved elements, like on plastic pipes. 34 | 35 | ### Using pathfinding 36 | 37 | In the component inspector, select the `Create` tab. Now in the scene view start dragging your cursor where you want the pipe to start, end let go where you want it to end; a pipe will appear. 38 | 39 | The tool uses A* pathfinding without a predefined grid — by raycasting from a point to the next points. Pipes can only detect colliders as obstacles. 40 | 41 | Property | Explanation 42 | :- | :- 43 | Preview Path | Will preview how the pipe will look like while dragging. Can be slow with complex pipes! 44 | Amount | How many pipes will be created at once; each one will have an individual path. 45 | Max Iterations | How many points will the algorithm check before giving up. 46 | Grid Y Angle | Rotates the Y axis of the pathfinding grid that every pipes have to follow. 47 | Grid Size | The distance between searched points; making it too small can produce bad results. 48 | Height | How high the first and the last segment of a pipe will be. This value can't be smaller than grid size. 49 | Chaos | Adds randomness to the pathfinding, making paths twisted and chaotic. 50 | Straight Priority | Makes the algorithm prefer straight paths over turns. 51 | Near Obstacle Priority | Makes the pipes stay close to obstacles. 52 | 53 | ![image](https://github.com/letharqic/InstantPipes/assets/44412176/a076dcf6-21d2-46b1-80c9-70cdbd59b00e) 54 | 55 | ### Manual Editing 56 | 57 | In the component inspector, select the `Edit` tab. Select one of the points in the scene view by clicking on it, and then you can: 58 | - Move the selected points in the scene view 59 | - Input the exact positions for the selected points in the inspector 60 | - Delete the point or the entire pipe via a button in the inspector 61 | - Insert a new point via a button in the inspector 62 | 63 | Hold `shift` to select multiple points. Press `A` to select every point of the selected pipe. 64 | 65 | Every pipe is a separate submesh, so you can assign separate materials by dragging them into the scene view. 66 | 67 | ## Troubleshooting 68 | 69 | > Getting an error when trying to build 70 | 71 | - That was recently patched, so please update the package or download the latest version of the tool. 72 | 73 | > Pipes appear squashed 74 | 75 | - Toggle and re-toggle rings, that should fix it. Will hopefully find a proper fix soon. 76 | 77 | > Dragging my cursor doesn't do anything 78 | 79 | - Make sure you have the `Create` tab selected 80 | - Make sure you're pointing at a surface with a collider 81 | - Make sure Gizmos are enabled 82 | - If nothing else helped, try resetting the editor layout to default 83 | 84 | > Pipes can't find a way 85 | 86 | - Set the `Iterations` higher, that will make the algorithm try for longer before giving up 87 | - Set the `Grid Size` higher, this way the algorithm can find a way with less iterations 88 | - Tone down the `Chaos`, `Straight Priority` and `Near Obstacle Priority` values, those make it harder to find a way 89 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ba44e7e70a5dcc44825be606b93b257 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d09725a81e4723648a02cc96fe4bd997 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/PathCreator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace InstantPipes 5 | { 6 | [System.Serializable] 7 | public class PathCreator 8 | { 9 | public float Height = 5; 10 | public float GridRotationY = 0; 11 | public float Radius = 1; 12 | public float GridSize = 3; 13 | public float Chaos = 0; 14 | public float StraightPathPriority = 10; 15 | public float NearObstaclesPriority = 0; 16 | public int MaxIterations = 1000; 17 | 18 | public bool LastPathSuccess = true; 19 | 20 | public List Create(Vector3 startPosition, Vector3 startNormal, Vector3 endPosition, Vector3 endNormal) 21 | { 22 | var path = new List(); 23 | var pathStart = startPosition + startNormal.normalized * Height; 24 | var pathEnd = endPosition + endNormal.normalized * Height; 25 | var baseDirection = (pathEnd - pathStart).normalized; 26 | 27 | var pathPoints = FindPath(new Point(pathStart), new Point(pathEnd), startNormal.normalized); 28 | 29 | path.Add(startPosition); 30 | path.Add(pathStart); 31 | 32 | LastPathSuccess = true; 33 | 34 | if (pathPoints != null) 35 | { 36 | pathPoints.ForEach(pathPoint => path.Add(pathPoint.Position)); 37 | } 38 | else 39 | { 40 | LastPathSuccess = false; 41 | } 42 | 43 | path.Add(pathEnd); 44 | path.Add(endPosition); 45 | 46 | return path; 47 | } 48 | 49 | private List FindPath(Point start, Point target, Vector3 startNormal) 50 | { 51 | var toSearch = new List { start }; 52 | var visited = new List(); 53 | var priorityFactor = start.GetDistanceTo(target) / 100; 54 | Random.InitState((int)(Chaos * 100)); 55 | 56 | Dictionary pointDictionary = new Dictionary(); 57 | 58 | int iterations = 0; 59 | while (toSearch.Count > 0 && iterations < MaxIterations) 60 | { 61 | iterations++; 62 | 63 | var current = toSearch[0]; 64 | foreach (var t in toSearch) 65 | if (t.F < current.F || t.F == current.F && t.H < current.H) current = t; 66 | 67 | visited.Add(current.Position); 68 | toSearch.Remove(current); 69 | 70 | if (Vector3.Distance(current.Position, target.Position) <= GridSize * 2) 71 | { 72 | var currentPathPoint = current; 73 | var path = new List(); 74 | while (currentPathPoint != start) 75 | { 76 | if (path.Count == 0 || !AreOnSameLine(path[path.Count - 1].Position, currentPathPoint.Position, currentPathPoint.Connection.Position)) 77 | { 78 | path.Add(currentPathPoint); 79 | } 80 | currentPathPoint = currentPathPoint.Connection; 81 | } 82 | path.Reverse(); 83 | return path; 84 | } 85 | 86 | var neighborPositions = current.GetNeighbors(GridSize, Radius, Quaternion.AngleAxis(GridRotationY, Vector3.up)); 87 | foreach (var position in neighborPositions) 88 | { 89 | if (!pointDictionary.ContainsKey(position)) 90 | { 91 | pointDictionary.Add(position, new Point(position)); 92 | } 93 | current.Neighbors.Add(pointDictionary[position]); 94 | } 95 | 96 | foreach (var neighbor in current.Neighbors) 97 | { 98 | if (ContainsVector(visited, neighbor.Position)) continue; 99 | 100 | var costToNeighbor = current.G + GridSize; 101 | 102 | if (current.Connection != null && (current.Connection.Position - current.Position).normalized != (current.Connection.Position - neighbor.Position).normalized) 103 | { 104 | costToNeighbor += StraightPathPriority * priorityFactor; 105 | } 106 | 107 | costToNeighbor += Random.Range(-Chaos, Chaos) * priorityFactor; 108 | 109 | if (NearObstaclesPriority != 0) 110 | { 111 | costToNeighbor += NearObstaclesPriority * neighbor.GetDistanceToNearestObstacle() * priorityFactor / 10; 112 | } 113 | 114 | 115 | if (!toSearch.Contains(neighbor) || costToNeighbor < neighbor.G) 116 | { 117 | neighbor.G = costToNeighbor; 118 | neighbor.Connection = current; 119 | 120 | if (!toSearch.Contains(neighbor)) 121 | { 122 | neighbor.H = neighbor.GetDistanceTo(target); 123 | toSearch.Add(neighbor); 124 | } 125 | } 126 | } 127 | } 128 | 129 | return null; 130 | } 131 | 132 | private bool AreOnSameLine(Vector3 point1, Vector3 point2, Vector3 point3) 133 | { 134 | return Vector3.Cross(point2 - point1, point3 - point1).sqrMagnitude < 0.0001f; 135 | } 136 | 137 | private bool ContainsVector(List list, Vector3 vector) 138 | { 139 | foreach (var pos in list) 140 | { 141 | if ((vector - pos).sqrMagnitude < 0.0001f) return true; 142 | } 143 | return false; 144 | } 145 | 146 | private class Point 147 | { 148 | public Vector3 Position; 149 | public List Neighbors; 150 | public Point Connection; 151 | 152 | public float G; 153 | public float H; 154 | public float F => G + H; 155 | 156 | private readonly Vector3[] _directions = { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back }; 157 | 158 | public Point(Vector3 position) 159 | { 160 | Position = position; 161 | Neighbors = new List(); 162 | } 163 | 164 | public float GetDistanceTo(Point other) 165 | { 166 | var distance = Vector3.Distance(other.Position, Position); 167 | return distance * 10; 168 | } 169 | 170 | public float GetDistanceToNearestObstacle() 171 | { 172 | float minDistance = 200; 173 | 174 | foreach (var direction in _directions) 175 | if (Physics.Raycast(Position, direction, out RaycastHit hitPoint, 200)) 176 | minDistance = Mathf.Min(minDistance, hitPoint.distance); 177 | 178 | return minDistance; 179 | } 180 | 181 | public List GetNeighbors(float gridSize, float radius, Quaternion rotation) 182 | { 183 | var list = new List(); 184 | 185 | foreach (var direction in _directions) 186 | { 187 | var rotatedDirection = rotation * direction; 188 | if (!Physics.SphereCast(Position, radius, rotatedDirection, out RaycastHit hit, gridSize + radius)) 189 | list.Add(Position + rotatedDirection * gridSize); 190 | } 191 | 192 | return list; 193 | } 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /Runtime/PathCreator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0aff3821e2451a4408b9460642635b36 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Pipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace InstantPipes 6 | { 7 | [System.Serializable] 8 | public class Pipe 9 | { 10 | public List Points; 11 | 12 | private List _verts; 13 | private List _normals; 14 | private List _uvs; 15 | private List _triIndices; 16 | private List _bezierPoints; 17 | private List _planes; 18 | private PipeGenerator _generator; 19 | 20 | private float _currentAngleOffset; 21 | private Quaternion _previousRotation; 22 | 23 | private float _ringThickness => _generator.HasExtrusion ? 0 : _generator.RingThickness; 24 | 25 | public Pipe(List points) 26 | { 27 | Points = points; 28 | } 29 | 30 | public float GetMaxDistanceBetweenPoints() 31 | { 32 | var maxDistance = 0f; 33 | for (int i = 1; i < Points.Count; i++) 34 | { 35 | maxDistance = Mathf.Max(maxDistance, Vector3.Distance(Points[i], Points[i - 1])); 36 | } 37 | return maxDistance; 38 | } 39 | 40 | public List GenerateMeshes(PipeGenerator generator) 41 | { 42 | var meshes = new List(); 43 | 44 | _generator = generator; 45 | 46 | ClearMeshInfo(); 47 | 48 | var ringPoints = new List(); 49 | 50 | var direction = (Points[0] - Points[1]).normalized; 51 | var rotation = (direction != Vector3.zero) ? Quaternion.LookRotation(direction, Vector3.up) : Quaternion.identity; 52 | _previousRotation = rotation; 53 | _bezierPoints.Add(new BezierPoint(Points[0], rotation)); 54 | 55 | for (int pipePoint = 1; pipePoint < Points.Count - 1; pipePoint++) 56 | { 57 | for (int s = 0; s < _generator.CurvedSegmentCount + 1; s++) 58 | { 59 | _bezierPoints.Add(GetBezierPoint((s / (float)_generator.CurvedSegmentCount), pipePoint)); 60 | if (s == 0 || s == _generator.CurvedSegmentCount) 61 | ringPoints.Add(_bezierPoints.Count - 1); 62 | } 63 | } 64 | 65 | _bezierPoints.Add(new BezierPoint(Points[Points.Count - 1], _previousRotation)); 66 | 67 | GenerateVertices(); 68 | GenerateUVs(); 69 | GenerateTriangles(); 70 | 71 | meshes.Add(new Mesh 72 | { 73 | vertices = _verts.ToArray(), 74 | normals = _normals.ToArray(), 75 | uv = _uvs.ToArray(), 76 | triangles = _triIndices.ToArray() 77 | }); 78 | _verts = new List(); 79 | _normals = new List(); 80 | _uvs = new List(); 81 | _triIndices = new List(); 82 | 83 | if (_generator.HasRings) 84 | { 85 | if (_generator.HasExtrusion) 86 | { 87 | GenerateVertices(isExtruded: true); 88 | GenerateUVs(); 89 | GenerateTriangles(isExtruded: true); 90 | 91 | for (int i = 1; i < _bezierPoints.Count - 1; i += _generator.CurvedSegmentCount + 1) 92 | { 93 | _planes.Add(new PlaneInfo(_bezierPoints[i], _generator.RingRadius + _generator.Radius, false)); 94 | if (i + _generator.CurvedSegmentCount >= _bezierPoints.Count) break; 95 | _planes.Add(new PlaneInfo(_bezierPoints[i + _generator.CurvedSegmentCount], _generator.RingRadius + _generator.Radius, true)); 96 | } 97 | } 98 | else 99 | { 100 | foreach (var point in ringPoints) GenerateDisc(_bezierPoints[point]); 101 | } 102 | } 103 | 104 | if (_generator.HasCaps) 105 | { 106 | GenerateDisc(_bezierPoints[_bezierPoints.Count - 1]); 107 | GenerateDisc(_bezierPoints[0]); 108 | } 109 | 110 | foreach (var plane in _planes) GeneratePlane(plane); 111 | 112 | meshes.Add(new Mesh 113 | { 114 | vertices = _verts.ToArray(), 115 | normals = _normals.ToArray(), 116 | uv = _uvs.ToArray(), 117 | triangles = _triIndices.ToArray() 118 | }); 119 | 120 | return meshes; 121 | } 122 | 123 | private void ClearMeshInfo() 124 | { 125 | _verts = new List(); 126 | _normals = new List(); 127 | _uvs = new List(); 128 | _triIndices = new List(); 129 | _bezierPoints = new List(); 130 | _planes = new List(); 131 | } 132 | 133 | private BezierPoint GetBezierPoint(float t, int x) 134 | { 135 | Vector3 prev, next; 136 | 137 | if ((Points[x] - Points[x - 1]).magnitude > _generator.Curvature * 2 + _ringThickness) 138 | prev = Points[x] - (Points[x] - Points[x - 1]).normalized * _generator.Curvature; 139 | else 140 | prev = (Points[x] + Points[x - 1]) / 2 + (Points[x] - Points[x - 1]).normalized * _ringThickness / 2; 141 | 142 | if ((Points[x] - Points[x + 1]).magnitude > _generator.Curvature * 2 + _ringThickness) 143 | next = Points[x] - (Points[x] - Points[x + 1]).normalized * _generator.Curvature; 144 | else 145 | next = (Points[x] + Points[x + 1]) / 2 + (Points[x] - Points[x + 1]).normalized * _ringThickness / 2; 146 | 147 | if (x == 1) 148 | { 149 | if ((Points[x] - Points[x - 1]).magnitude > _generator.Curvature + _ringThickness * 2.5f) 150 | prev = Points[x] - (Points[x] - Points[x - 1]).normalized * _generator.Curvature; 151 | else 152 | prev = Points[x - 1] + (Points[x] - Points[x - 1]).normalized * _ringThickness * 2.5f; 153 | } 154 | 155 | else if (x == Points.Count - 2) 156 | { 157 | if ((Points[x] - Points[x + 1]).magnitude > _generator.Curvature + _ringThickness * 2.5f) 158 | next = Points[x] - (Points[x] - Points[x + 1]).normalized * _generator.Curvature; 159 | else 160 | next = Points[x + 1] + (Points[x] - Points[x + 1]).normalized * _ringThickness * 2.5f; 161 | } 162 | 163 | Vector3 a = Vector3.Lerp(prev, Points[x], t); 164 | Vector3 b = Vector3.Lerp(Points[x], next, t); 165 | var position = Vector3.Lerp(a, b, t); 166 | 167 | Vector3 aNext = Vector3.LerpUnclamped(prev, Points[x], t + 0.001f); 168 | Vector3 bNext = Vector3.LerpUnclamped(Points[x], next, t + 0.001f); 169 | 170 | var tangent = Vector3.Cross(a - b, aNext - bNext); 171 | var rotation = (a != b) ? Quaternion.LookRotation((a - b).normalized, tangent) : Quaternion.identity; 172 | 173 | // Rotate new tangent along the forward axis to match the previous part 174 | 175 | if (t == 0) 176 | { 177 | _currentAngleOffset = Quaternion.Angle(_previousRotation, rotation); 178 | var offsetRotation = Quaternion.AngleAxis(_currentAngleOffset, Vector3.forward); 179 | if (Quaternion.Angle(rotation * offsetRotation, _previousRotation) > 0) 180 | _currentAngleOffset *= -1; 181 | } 182 | rotation *= Quaternion.AngleAxis(_currentAngleOffset, Vector3.forward); 183 | 184 | _previousRotation = rotation; 185 | return new BezierPoint(position, rotation); 186 | } 187 | 188 | private void GenerateUVs() 189 | { 190 | float length = 0; 191 | for (int i = 1; i < _bezierPoints.Count; i++) 192 | length += (_bezierPoints[i].Pos - _bezierPoints[i - 1].Pos).magnitude; 193 | 194 | float currentUV = 0; 195 | for (int i = 0; i < _bezierPoints.Count; i++) 196 | { 197 | if (i != 0) 198 | currentUV += (_bezierPoints[i].Pos - _bezierPoints[i - 1].Pos).magnitude / length; 199 | 200 | for (int edge = 0; edge < _generator.EdgeCount; edge++) 201 | { 202 | _uvs.Add(new Vector2(edge / (float)_generator.EdgeCount, currentUV * length)); 203 | } 204 | _uvs.Add(new Vector2(1, currentUV * length)); 205 | } 206 | } 207 | 208 | private void GenerateVertices(bool isExtruded = false) 209 | { 210 | for (int point = 0; point < _bezierPoints.Count; point++) 211 | { 212 | for (int i = 0; i < _generator.EdgeCount; i++) 213 | { 214 | float t = i / (float)_generator.EdgeCount; 215 | float angRad = t * 6.2831853f; 216 | Vector3 direction = new Vector3(Mathf.Sin(angRad), Mathf.Cos(angRad), 0); 217 | _normals.Add(_bezierPoints[point].LocalToWorldVector(direction.normalized)); 218 | _verts.Add(_bezierPoints[point].LocalToWorldPosition(direction * (isExtruded ? _generator.RingRadius + _generator.Radius : _generator.Radius))); 219 | } 220 | 221 | // Extra vertice to fix smoothed UVs 222 | 223 | _normals.Add(_normals[_normals.Count - _generator.EdgeCount]); 224 | _verts.Add(_verts[_verts.Count - _generator.EdgeCount]); 225 | } 226 | } 227 | 228 | private void GenerateTriangles(bool isExtruded = false) 229 | { 230 | var edges = _generator.EdgeCount + 1; 231 | for (int s = 0; s < _bezierPoints.Count - 1; s++) 232 | { 233 | if (isExtruded && s % (_generator.CurvedSegmentCount + 1) == 0) continue; 234 | if (!isExtruded && _generator.HasRings && _generator.HasExtrusion && s % (_generator.CurvedSegmentCount + 1) != 0) continue; 235 | 236 | int rootIndex = s * edges; 237 | int rootIndexNext = (s + 1) * edges; 238 | for (int i = 0; i < edges; i++) 239 | { 240 | int currentA = rootIndex + i; 241 | int currentB = rootIndex + (i + 1) % edges; 242 | int nextA = rootIndexNext + i; 243 | int nextB = rootIndexNext + (i + 1) % edges; 244 | 245 | _triIndices.Add(nextB); 246 | _triIndices.Add(nextA); 247 | _triIndices.Add(currentA); 248 | _triIndices.Add(currentB); 249 | _triIndices.Add(nextB); 250 | _triIndices.Add(currentA); 251 | } 252 | } 253 | } 254 | 255 | private void GenerateDisc(BezierPoint point) 256 | { 257 | var rootIndex = _verts.Count; 258 | bool isFirst = (point.Pos == _bezierPoints[0].Pos); 259 | bool isLast = (point.Pos == _bezierPoints[_bezierPoints.Count - 1].Pos); 260 | 261 | if (isFirst) 262 | point.Pos -= point.LocalToWorldVector(Vector3.forward) * (_generator.CapThickness + _generator.CapOffset); 263 | else if (isLast) 264 | point.Pos += point.LocalToWorldVector(Vector3.forward) * _generator.CapOffset; 265 | else 266 | point.Pos -= point.LocalToWorldVector(Vector3.forward) * _ringThickness / 2; 267 | 268 | var radius = (isLast || isFirst) ? _generator.CapRadius + _generator.Radius : _generator.RingRadius + _generator.Radius; 269 | var uv = (isLast || isFirst) ? _generator.CapThickness : _ringThickness; 270 | 271 | for (int p = 0; p < 2; p++) 272 | { 273 | for (int i = 0; i < _generator.EdgeCount; i++) 274 | { 275 | float t = i / (float)_generator.EdgeCount; 276 | float angRad = t * 6.2831853f; 277 | Vector3 direction = new Vector3(Mathf.Sin(angRad), Mathf.Cos(angRad), 0); 278 | _normals.Add(point.LocalToWorldVector(direction.normalized)); 279 | _verts.Add(point.LocalToWorldPosition(direction * radius)); 280 | _uvs.Add(new Vector2(t, uv * p)); 281 | } 282 | 283 | _normals.Add(_normals[_normals.Count - _generator.EdgeCount]); 284 | _verts.Add(_verts[_verts.Count - _generator.EdgeCount]); 285 | _uvs.Add(new Vector2(1, uv * p)); 286 | 287 | _planes.Add(new PlaneInfo(point, radius, p == 0)); 288 | 289 | if (isLast || isFirst) 290 | point.Pos += point.LocalToWorldVector(Vector3.forward) * _generator.CapThickness; 291 | else 292 | point.Pos += point.LocalToWorldVector(Vector3.forward) * _ringThickness; 293 | 294 | } 295 | 296 | var edges = _generator.EdgeCount + 1; 297 | 298 | for (int i = 0; i < edges; i++) 299 | { 300 | _triIndices.Add(i + rootIndex); 301 | _triIndices.Add(edges + i + rootIndex); 302 | _triIndices.Add(edges + (i + 1) % edges + rootIndex); 303 | _triIndices.Add(i + rootIndex); 304 | _triIndices.Add(edges + (i + 1) % edges + rootIndex); 305 | _triIndices.Add((i + 1) % edges + rootIndex); 306 | } 307 | } 308 | 309 | private void GeneratePlane(PlaneInfo plane) 310 | { 311 | var edges = _generator.EdgeCount + 1; 312 | var rootIndex = _verts.Count; 313 | 314 | var planePointVectors = new List(); 315 | 316 | for (int p = 0; p < 2; p++) 317 | { 318 | for (int i = 0; i < _generator.EdgeCount; i++) 319 | { 320 | float t = i / (float)_generator.EdgeCount; 321 | float angRad = t * 6.2831853f; 322 | Vector3 direction = new Vector3(Mathf.Sin(angRad), Mathf.Cos(angRad), 0); 323 | planePointVectors.Add(direction); 324 | } 325 | planePointVectors.Add(planePointVectors[planePointVectors.Count - 1]); 326 | } 327 | 328 | for (int i = 0; i < planePointVectors.Count; i++) 329 | { 330 | _verts.Add(plane.Point.LocalToWorldPosition(planePointVectors[i] * plane.Radius)); 331 | if (i > _generator.EdgeCount) 332 | _normals.Add(plane.Point.LocalToWorldVector(Vector3.forward)); 333 | else 334 | _normals.Add(plane.Point.LocalToWorldVector(Vector3.back)); 335 | _uvs.Add(planePointVectors[i] * _generator.RingsUVScale); 336 | } 337 | 338 | for (int i = 1; i < edges - 1; i++) 339 | { 340 | if (plane.IsForward) 341 | { 342 | _triIndices.Add(0 + rootIndex); 343 | _triIndices.Add(i + rootIndex); 344 | _triIndices.Add(i + 1 + rootIndex); 345 | } 346 | else 347 | { 348 | _triIndices.Add(edges + i + 1 + rootIndex); 349 | _triIndices.Add(edges + i + rootIndex); 350 | _triIndices.Add(edges + rootIndex); 351 | } 352 | } 353 | } 354 | 355 | private struct BezierPoint 356 | { 357 | public Vector3 Pos; 358 | public Quaternion Rot; 359 | 360 | public BezierPoint(Vector3 pos, Quaternion rot) 361 | { 362 | this.Pos = pos; 363 | this.Rot = rot; 364 | } 365 | 366 | public Vector3 LocalToWorldPosition(Vector3 localSpacePos) => Rot * localSpacePos + Pos; 367 | public Vector3 LocalToWorldVector(Vector3 localSpacePos) => Rot * localSpacePos; 368 | } 369 | 370 | private struct PlaneInfo 371 | { 372 | public BezierPoint Point; 373 | public float Radius; 374 | public bool IsForward; 375 | 376 | public PlaneInfo(BezierPoint point, float radius, bool isForward) 377 | { 378 | this.Point = point; 379 | this.Radius = radius; 380 | this.IsForward = isForward; 381 | } 382 | } 383 | } 384 | } -------------------------------------------------------------------------------- /Runtime/Pipe.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 44c68ab948fb1154b894f80b2dbd4f6b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/PipeGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace InstantPipes 5 | { 6 | [ExecuteAlways] 7 | [RequireComponent(typeof(MeshFilter))] 8 | [RequireComponent(typeof(MeshCollider))] 9 | [RequireComponent(typeof(MeshRenderer))] 10 | public class PipeGenerator : MonoBehaviour 11 | { 12 | public float Radius = 1; 13 | public int EdgeCount = 10; 14 | public int CurvedSegmentCount = 10; 15 | public float Curvature = 0.5f; 16 | 17 | public bool HasRings; 18 | public bool HasExtrusion; 19 | public float RingThickness = 1; 20 | public float RingRadius = 1.3f; 21 | 22 | public bool HasCaps; 23 | public float CapThickness = 1; 24 | public float CapRadius = 1.3f; 25 | public float CapOffset = 0f; 26 | 27 | public int PipesAmount = 1; 28 | 29 | public Material Material; 30 | public Material RingMaterial; 31 | public float RingsUVScale = 1; 32 | public bool IsSeparateRingsMaterial = false; 33 | 34 | private Renderer _renderer; 35 | private MeshCollider _collider; 36 | private Mesh _mesh; 37 | 38 | public List Pipes = new List(); 39 | public PathCreator PathCreator = new PathCreator(); 40 | 41 | private float _maxDistanceBetweenPoints; 42 | public float MaxCurvature => _maxDistanceBetweenPoints / 2; 43 | 44 | private void OnEnable() 45 | { 46 | _renderer = GetComponent(); 47 | _collider = GetComponent(); 48 | 49 | _mesh = new Mesh { name = "Pipes" }; 50 | GetComponent().sharedMesh = _mesh; 51 | _collider.sharedMesh = _mesh; 52 | 53 | UpdateMesh(); 54 | } 55 | 56 | public void UpdateMesh() 57 | { 58 | _maxDistanceBetweenPoints = 0; 59 | _collider.sharedMesh = null; 60 | 61 | var submeshes = new List(); 62 | foreach (var pipe in Pipes) 63 | { 64 | _mesh.Clear(); 65 | 66 | var meshes = pipe.GenerateMeshes(this); 67 | meshes.ForEach(mesh => submeshes.Add(new CombineInstance { mesh = mesh })); 68 | _mesh.CombineMeshes(submeshes.ToArray(), false, false); 69 | 70 | _collider.sharedMesh = _mesh; 71 | 72 | _maxDistanceBetweenPoints = Mathf.Max(_maxDistanceBetweenPoints, pipe.GetMaxDistanceBetweenPoints()); 73 | } 74 | 75 | var materials = new List(); 76 | if (IsSeparateRingsMaterial && (HasCaps || HasRings)) 77 | { 78 | for (int i = 0; i < Pipes.Count * 2; i++) materials.Add(i % 2 == 0 ? Material : RingMaterial); 79 | } 80 | else if (HasCaps || HasRings) 81 | { 82 | for (int i = 0; i < Pipes.Count * 2; i++) materials.Add(Material); 83 | } 84 | else 85 | { 86 | for (int i = 0; i < Pipes.Count; i++) materials.Add(Material); 87 | } 88 | _renderer.sharedMaterials = materials.ToArray(); 89 | } 90 | 91 | public bool AddPipe(Vector3 startPoint, Vector3 startNormal, Vector3 endPoint, Vector3 endNormal) 92 | { 93 | var failed = false; 94 | 95 | var vectorLength = PipesAmount * Radius + (PipesAmount - 1) * PathCreator.GridSize; 96 | var startVector = Vector3.Cross(startNormal, endPoint - startPoint).normalized * vectorLength; 97 | var endVector = Vector3.Cross(endNormal, endPoint - startPoint).normalized * vectorLength; 98 | var stepSize = startVector.magnitude / (PipesAmount); 99 | 100 | var temporaryColliders = new List(); 101 | 102 | for (int i = 0; i < PipesAmount; i++) 103 | { 104 | var start = startVector.normalized * (stepSize * (i - PipesAmount / 2f + 0.5f)); 105 | var end = endVector.normalized * (stepSize * (i - PipesAmount / 2f + 0.5f)); 106 | 107 | temporaryColliders.Add(CreateTemporaryCollider(startPoint + start, startNormal)); 108 | temporaryColliders.Add(CreateTemporaryCollider(endPoint + end, endNormal)); 109 | } 110 | 111 | try 112 | { 113 | for (int i = 0; i < PipesAmount; i++) 114 | { 115 | var start = startVector.normalized * (stepSize * (i - PipesAmount / 2f + 0.5f)); 116 | var end = endVector.normalized * (stepSize * (i - PipesAmount / 2f + 0.5f)); 117 | 118 | Pipes.Add(new Pipe(PathCreator.Create(start + startPoint, startNormal, end + endPoint, endNormal))); 119 | if (!PathCreator.LastPathSuccess) failed = true; 120 | UpdateMesh(); 121 | } 122 | } 123 | finally 124 | { 125 | temporaryColliders.ForEach(collider => Object.DestroyImmediate(collider)); 126 | } 127 | 128 | return !failed; 129 | } 130 | 131 | public void InsertPoint(int pipeIndex, int pointIndex) 132 | { 133 | var position = Vector3.zero; 134 | if (pointIndex != Pipes[pipeIndex].Points.Count - 1) 135 | position = (Pipes[pipeIndex].Points[pointIndex + 1] + Pipes[pipeIndex].Points[pointIndex]) / 2; 136 | else 137 | position = Pipes[pipeIndex].Points[pointIndex] + Vector3.one; 138 | Pipes[pipeIndex].Points.Insert(pointIndex + 1, position); 139 | UpdateMesh(); 140 | } 141 | 142 | public void RemovePoint(int pipeIndex, int pointIndex) 143 | { 144 | Pipes[pipeIndex].Points.RemoveAt(pointIndex); 145 | UpdateMesh(); 146 | } 147 | 148 | public void RemovePipe(int pipeIndex) 149 | { 150 | Pipes.RemoveAt(pipeIndex); 151 | UpdateMesh(); 152 | } 153 | 154 | public bool RegeneratePaths() 155 | { 156 | var pipesCopy = new List(Pipes); 157 | Pipes = new List(); 158 | UpdateMesh(); 159 | var failed = false; 160 | 161 | var temporaryColliders = new List(); 162 | 163 | foreach (var pipe in pipesCopy) 164 | { 165 | temporaryColliders.Add(CreateTemporaryCollider(pipe.Points[0], (pipe.Points[1] - pipe.Points[0]).normalized)); 166 | temporaryColliders.Add(CreateTemporaryCollider(pipe.Points[pipe.Points.Count - 1], (pipe.Points[pipe.Points.Count - 2] - pipe.Points[pipe.Points.Count - 1]).normalized)); 167 | } 168 | 169 | try 170 | { 171 | foreach (var pipe in pipesCopy) 172 | { 173 | Pipes.Add(new Pipe(PathCreator.Create(pipe.Points[0], pipe.Points[1] - pipe.Points[0], pipe.Points[pipe.Points.Count - 1], pipe.Points[pipe.Points.Count - 2] - pipe.Points[pipe.Points.Count - 1]))); 174 | if (!PathCreator.LastPathSuccess) failed = true; 175 | UpdateMesh(); 176 | } 177 | } 178 | finally 179 | { 180 | temporaryColliders.ForEach(collider => Object.DestroyImmediate(collider)); 181 | } 182 | 183 | return !failed; 184 | } 185 | 186 | private GameObject CreateTemporaryCollider(Vector3 point, Vector3 normal) 187 | { 188 | var tempCollider = new GameObject(); 189 | tempCollider.transform.position = point + (normal * PathCreator.Height) / 2; 190 | tempCollider.transform.localScale = new Vector3(Radius * 2, PathCreator.Height - Radius * 3f, Radius * 2); 191 | tempCollider.transform.rotation = Quaternion.FromToRotation(Vector3.up, normal); 192 | tempCollider.AddComponent(); 193 | return tempCollider; 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /Runtime/PipeGenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff9d8077e85388443ae15b3a9424709f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/leth.instantpipes.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leth.instantpipes", 3 | "rootNamespace": "InstantPipes", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [], 13 | "noEngineReferences": false 14 | } -------------------------------------------------------------------------------- /Runtime/leth.instantpipes.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 507c31c94ab77a54fb5ef52f553b2f5a 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.leth.instantpipes", 3 | "version": "1.0.0", 4 | "displayName": "InstantPipes", 5 | "description": "An editor tool for procedurally generating pipes by just dragging the cursor from start to end — the pipe will find the path in a customizable way.", 6 | "unity": "2019.4" 7 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 343ade3ac7136a94c904c7257c9b9c2f 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------