365 lines
13 KiB
C#
365 lines
13 KiB
C#
// ╔════════════════════════════════════════════════════════════════╗
|
|
// ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║
|
|
// ║ Licensed under Unity Asset Store Terms of Service: ║
|
|
// ║ https://unity.com/legal/as-terms ║
|
|
// ║ Use permitted only in compliance with the License. ║
|
|
// ║ Distributed "AS IS", without warranty of any kind. ║
|
|
// ╚════════════════════════════════════════════════════════════════╝
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
#region
|
|
|
|
using NWH.NUI;
|
|
using NWH.DWP2.WaterObjects;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
#endregion
|
|
|
|
namespace NWH.DWP2
|
|
{
|
|
/// <summary>
|
|
/// Custom inspector for WaterObject.
|
|
/// </summary>
|
|
[CustomEditor(typeof(WaterObject))]
|
|
[CanEditMultipleObjects]
|
|
[ExecuteAlways]
|
|
public class WaterObjectEditor : DWP2NUIEditor
|
|
{
|
|
private bool _editorHasWarnings;
|
|
private Texture2D _greyTexture;
|
|
private Texture2D _originalMeshPreviewTexture;
|
|
private float _previewHeight;
|
|
private Texture2D _simMeshPreviewTexture;
|
|
|
|
private WaterObject _waterObject;
|
|
|
|
|
|
private void OnEnable()
|
|
{
|
|
_greyTexture = new Texture2D(10, 10);
|
|
FillInTexture(ref _greyTexture, new Color32(66, 66, 66, 255));
|
|
}
|
|
|
|
|
|
private static void FillInTexture(ref Texture2D tex, Color color)
|
|
{
|
|
Color[] fillColorArray = tex.GetPixels();
|
|
|
|
for (int i = 0; i < fillColorArray.Length; i++)
|
|
{
|
|
fillColorArray[i] = color;
|
|
}
|
|
|
|
tex.SetPixels(fillColorArray);
|
|
tex.Apply();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Draws custom inspector GUI for WaterObject.
|
|
/// </summary>
|
|
public override bool OnInspectorNUI()
|
|
{
|
|
if (!base.OnInspectorNUI())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get water object and make sure it is initialized
|
|
_waterObject = (WaterObject)target;
|
|
|
|
// Draw logo texture
|
|
Rect logoRect = drawer.positionRect;
|
|
logoRect.height = 60f;
|
|
drawer.DrawEditorTexture(logoRect, "Dynamic Water Physics 2/Logos/WaterObjectLogo");
|
|
drawer.AdvancePosition(logoRect.height);
|
|
|
|
if (_waterObject.originalMesh == null || _waterObject.serializedSimulationMesh == null ||
|
|
_waterObject.SimulationMesh == null)
|
|
{
|
|
drawer.Space();
|
|
drawer.Info("Simulation mesh not generated. Click 'Update Simulation Mesh' button below to fix this.",
|
|
MessageType.Error);
|
|
}
|
|
|
|
if (_waterObject.CurrentWaterDataProvider)
|
|
{
|
|
drawer.Info($"Current water data source: '{_waterObject.CurrentWaterDataProvider.GetType().Name}' " +
|
|
$"on object '{_waterObject.CurrentWaterDataProvider.name}'.");
|
|
}
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
drawer.Info($"Forces are being applied to '{_waterObject.targetRigidbody}'.");
|
|
}
|
|
|
|
drawer.BeginSubsection("Buoyancy");
|
|
if (Application.isPlaying)
|
|
{
|
|
drawer.Field("submergedVolume", false, "l");
|
|
}
|
|
|
|
drawer.Field("buoyantForceCoefficient");
|
|
drawer.Field("fluidDensity");
|
|
drawer.EndSubsection();
|
|
|
|
drawer.BeginSubsection("Hydrodynamics");
|
|
drawer.Field("hydrodynamicForceCoefficient");
|
|
drawer.Field("slamForceCoefficient");
|
|
drawer.Field("suctionForceCoefficient");
|
|
drawer.Field("skinDragCoefficient");
|
|
drawer.Field("velocityDotPower");
|
|
drawer.EndSubsection();
|
|
|
|
drawer.BeginSubsection("Water");
|
|
drawer.Field("calculateWaterHeights");
|
|
drawer.Field("calculateWaterNormals");
|
|
drawer.Field("calculateWaterFlows");
|
|
|
|
drawer.Field("defaultWaterHeight");
|
|
drawer.Field("defaultWaterNormal");
|
|
drawer.Field("defaultWaterFlow");
|
|
drawer.EndSubsection();
|
|
|
|
// Simulation mesh
|
|
drawer.BeginSubsection("Simulation Mesh Settings");
|
|
if (drawer.Field("simplifyMesh").boolValue)
|
|
{
|
|
drawer.Field("targetTriangleCount");
|
|
}
|
|
|
|
drawer.Field("convexifyMesh");
|
|
drawer.Field("weldColocatedVertices");
|
|
|
|
if (drawer.Button("Update Simulation Mesh"))
|
|
{
|
|
UpdateSimulationMesh();
|
|
}
|
|
|
|
if (drawer.Button("Toggle In-Scene Preview"))
|
|
{
|
|
ToggleInScenePreview();
|
|
}
|
|
|
|
if (targets.Length == 1)
|
|
{
|
|
drawer.Label("Simulation Mesh Preview:");
|
|
drawer.Space();
|
|
if (Event.current.type == EventType.Repaint)
|
|
{
|
|
DrawPreviewTexture(_waterObject, drawer.positionRect, out _previewHeight);
|
|
}
|
|
|
|
drawer.Space(_previewHeight);
|
|
}
|
|
|
|
drawer.EndSubsection();
|
|
|
|
// Warnings
|
|
drawer.BeginSubsection("Messages");
|
|
DrawWarnings();
|
|
drawer.EndSubsection();
|
|
|
|
drawer.EndEditor(this);
|
|
return true;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Draws validation warnings and errors.
|
|
/// </summary>
|
|
private void DrawWarnings()
|
|
{
|
|
if (targets.Length == 1)
|
|
{
|
|
_editorHasWarnings = false;
|
|
|
|
// Missing rigidbody
|
|
if (_waterObject.targetRigidbody == null)
|
|
{
|
|
_waterObject.targetRigidbody = _waterObject.transform.GetComponentInParent<Rigidbody>(true);
|
|
if (_waterObject.targetRigidbody == null)
|
|
{
|
|
drawer.Info("WaterObject requires a rigidbody attached to the object or its parent(s) to " +
|
|
$"function. Add a rigidbody to object {_waterObject.name} or one of its parents.",
|
|
MessageType.Error);
|
|
_editorHasWarnings = true;
|
|
|
|
if (drawer.Button("Add a Rigidbody"))
|
|
{
|
|
foreach (WaterObject wo in targets)
|
|
{
|
|
wo.gameObject.AddComponent<Rigidbody>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collider count
|
|
if (_waterObject.targetRigidbody != null)
|
|
{
|
|
int colliderCount = _waterObject.targetRigidbody.transform.GetComponentsInChildren<Collider>()
|
|
.Length;
|
|
if (_waterObject.targetRigidbody.transform.GetComponentsInChildren<Collider>().Length == 0)
|
|
{
|
|
drawer.Info(
|
|
$"Found {colliderCount} colliders attached to rigidbody {_waterObject.targetRigidbody.name} " +
|
|
"and its children. At least one collider is required for a rigidbody to work properly.",
|
|
MessageType.Error);
|
|
_editorHasWarnings = true;
|
|
|
|
if (drawer.Button("Add a MeshCollider"))
|
|
{
|
|
foreach (WaterObject wo in targets)
|
|
{
|
|
MeshCollider mc = wo.gameObject.AddComponent<MeshCollider>();
|
|
mc.convex = true;
|
|
mc.isTrigger = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Excessive triangle count
|
|
if (_waterObject.triangleCount > 150)
|
|
{
|
|
drawer.Info($"Possible excessive number of triangles detected ({_waterObject.triangleCount})." +
|
|
" Use simplify mesh option to reduce the number of triangles, or if this is intentional ignore this message." +
|
|
" Recommended number is 16-128.", MessageType.Warning);
|
|
}
|
|
|
|
// Scale error
|
|
if (_waterObject.transform.localScale.x <= 0
|
|
|| _waterObject.transform.localScale.y <= 0
|
|
|| _waterObject.transform.localScale.z <= 0)
|
|
{
|
|
drawer.Info(
|
|
"Scale of this object is negative or zero on one or more of axes. Scale less than or equal to zero is not supported." +
|
|
" WaterObject will still be calculated but with unpredictable results. ", MessageType.Error);
|
|
}
|
|
|
|
if (!_editorHasWarnings)
|
|
{
|
|
drawer.Info($"No warnings or errors for object {_waterObject.name}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Regenerates simulation mesh for selected objects.
|
|
/// </summary>
|
|
private void UpdateSimulationMesh()
|
|
{
|
|
foreach (WaterObject wo in targets)
|
|
{
|
|
wo.StopSimMeshPreview();
|
|
wo.GenerateSimMesh();
|
|
Undo.RecordObject(wo, "Updated Simulation Mesh");
|
|
EditorUtility.SetDirty(wo);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Toggles in-scene preview of simulation mesh.
|
|
/// </summary>
|
|
private void ToggleInScenePreview()
|
|
{
|
|
foreach (WaterObject wo in targets)
|
|
{
|
|
if (wo.PreviewEnabled)
|
|
{
|
|
wo.StopSimMeshPreview();
|
|
}
|
|
else
|
|
{
|
|
wo.StartSimMeshPreview();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Draws side-by-side preview of original and simulation meshes.
|
|
/// </summary>
|
|
private void DrawPreviewTexture(WaterObject waterObject, Rect rect, out float previewHeight)
|
|
{
|
|
previewHeight = 0;
|
|
|
|
if (waterObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (waterObject.originalMesh == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (waterObject.serializedSimulationMesh.vertices == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (waterObject.SimulationMesh == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Tri count
|
|
int originalTriCount = waterObject.originalMesh == null ? 0 : waterObject.originalMesh.triangles.Length / 3;
|
|
int originalVertCount = waterObject.originalMesh == null ? 0 : waterObject.originalMesh.vertices.Length;
|
|
int simulationTriCount = waterObject.serializedSimulationMesh.triangles.Length / 3;
|
|
int simulationVertCount = waterObject.serializedSimulationMesh.vertices.Length;
|
|
|
|
_originalMeshPreviewTexture = AssetPreview.GetAssetPreview(waterObject.originalMesh);
|
|
_simMeshPreviewTexture = waterObject.SimulationMesh == null
|
|
? AssetPreview.GetAssetPreview(waterObject.originalMesh)
|
|
: AssetPreview.GetAssetPreview(waterObject.SimulationMesh);
|
|
|
|
float startY = rect.y;
|
|
float previewWidth = rect.width;
|
|
float maxPreviewWidth = 480f;
|
|
previewWidth = Mathf.Clamp(previewWidth, 240f, maxPreviewWidth);
|
|
float margin = (rect.width - previewWidth) * 0.5f;
|
|
float halfWidth = previewWidth * 0.5f;
|
|
|
|
Rect leftRect = new(rect.x + margin, startY, halfWidth, halfWidth);
|
|
Rect rightRect = new(rect.x + halfWidth + margin, startY, halfWidth, halfWidth);
|
|
|
|
Material previewMaterial = new(Shader.Find("UI/Default"));
|
|
|
|
GUI.DrawTexture(leftRect, _originalMeshPreviewTexture == null ? _greyTexture : _originalMeshPreviewTexture);
|
|
GUI.DrawTexture(rightRect, _simMeshPreviewTexture == null ? _greyTexture : _simMeshPreviewTexture);
|
|
|
|
GUIStyle centeredStyle = GUI.skin.GetStyle("Label");
|
|
centeredStyle.alignment = TextAnchor.MiddleCenter;
|
|
centeredStyle.normal.textColor = Color.white;
|
|
|
|
Rect leftLabelRect = leftRect;
|
|
leftLabelRect.height = 20f;
|
|
GUI.Label(leftLabelRect, "ORIGINAL", centeredStyle);
|
|
|
|
Rect rightLabelRect = rightRect;
|
|
rightLabelRect.height = 20f;
|
|
GUI.Label(rightLabelRect, "SIMULATION", centeredStyle);
|
|
|
|
Rect leftBottomLabelRect = leftRect;
|
|
leftBottomLabelRect.y = leftRect.y + halfWidth - 20f;
|
|
leftBottomLabelRect.height = 20f;
|
|
GUI.Label(leftBottomLabelRect, $"{originalTriCount} tris, {originalVertCount} verts");
|
|
|
|
Rect rightBottomLabelrect = rightRect;
|
|
rightBottomLabelrect.y = rightRect.y + halfWidth - 20f;
|
|
rightBottomLabelrect.height = 20f;
|
|
GUI.Label(rightBottomLabelrect, $"{simulationTriCount} tris, {simulationVertCount} verts");
|
|
|
|
previewHeight = halfWidth;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif |