You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
21 KiB
C#
467 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Unity.VisualScripting;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
using UnityEngine;
|
|
using UnityEngine.Formats.Alembic.Importer;
|
|
|
|
namespace FluffyGroomingTool {
|
|
public class AlembicSetupWindow : EditorWindow {
|
|
[MenuItem("Tools/Fluffy Grooming Tool/Alembic Groom Setup", false, 2)]
|
|
public static AlembicSetupWindow launchFurPainter() {
|
|
var window = GetWindowWithRect<AlembicSetupWindow>(new Rect(0, 0, 600, 665));
|
|
window.titleContent = new GUIContent("Alembic Setup");
|
|
window.Show();
|
|
return window;
|
|
}
|
|
|
|
private SerializedProperty sourceGamObject;
|
|
private SerializedProperty alembicGroom;
|
|
private SerializedProperty dontSkin;
|
|
private GameObject helper;
|
|
private SerializedObject seriaLizedObject;
|
|
private Editor sourceEditor;
|
|
private GameObject combinedObject;
|
|
|
|
private GUIStyle headingStyle;
|
|
private GUIStyle textStyle;
|
|
private GUIStyle panelStyle;
|
|
private GUIStyle panelOutlineStyle;
|
|
private GUIStyle vignetteStyle;
|
|
private GUIStyle buttonStyle;
|
|
private GUIStyle buttonStyleSelected;
|
|
private readonly EditorDeltaTime editorDeltaTime = new EditorDeltaTime();
|
|
private List<Renderer> renderersInSourceMesh;
|
|
private Vector2 scroll;
|
|
private static readonly float BUILD_BUTTON_SCALE = 0.34f;
|
|
private static readonly float BUILD_BUTTON_WIDTH = 618 * BUILD_BUTTON_SCALE;
|
|
private static readonly float BUILD_BUTTON_HEIGHT = 150f * BUILD_BUTTON_SCALE;
|
|
|
|
private ImportantButton startSkinningButton;
|
|
private ToastMessage selectionLockedMessage = new ToastMessage();
|
|
private int selectedRendererIndex;
|
|
private Mesh errorMesh;
|
|
private bool isMeshInWrongFormat;
|
|
|
|
private void skinAndCreateObject() {
|
|
var curveScripts = combinedObject.GetComponentsInChildren<AlembicCurves>();
|
|
if (curveScripts.Length == 0) ErrorLogger.logNoCurvesFound();
|
|
var points = new List<Vector3>();
|
|
var uvs = new List<Vector2>();
|
|
foreach (var cs in curveScripts) {
|
|
points.AddRange(cs.Positions);
|
|
uvs.AddRange(cs.UVs);
|
|
}
|
|
|
|
var pointsPerStrand = curveScripts[0].CurveOffsets[1];
|
|
if (!dontSkin.boolValue) {
|
|
var skinToObject = renderersInSourceMesh[selectedRendererIndex].gameObject;
|
|
var hairContainer = HairContainer.createFromAlembicAndSkin(points.ToArray(), pointsPerStrand, skinToObject);
|
|
var newObject = (GameObject) Instantiate(sourceGamObject.objectReferenceValue);
|
|
newObject.name = sourceGamObject.objectReferenceValue.name;
|
|
var names = new List<string>();
|
|
parentNames(skinToObject.transform, ref names);
|
|
names.RemoveAt(0);
|
|
var parentNamesS = String.Join("/", names.ToArray());
|
|
skinToObject = parentNamesS.Length == 0 ? newObject : newObject.transform.Find(parentNamesS).gameObject;
|
|
|
|
var hairRenderer = skinToObject.AddComponent<HairRenderer>();
|
|
finalizeAndSave(hairContainer, skinToObject, hairRenderer, newObject);
|
|
}
|
|
else {
|
|
var newObject = new GameObject {name = alembicGroom.name + "HairRenderer"};
|
|
var hairContainer = HairContainer.createFromAlembicWithoutSkinning(points.ToArray(), pointsPerStrand, uvs);
|
|
var hairRenderer = newObject.AddComponent<HairRenderer>();
|
|
finalizeAndSave(hairContainer, newObject, hairRenderer, newObject);
|
|
}
|
|
}
|
|
|
|
private bool closeWindow;
|
|
|
|
private void finalizeAndSave(HairContainer hairContainer, GameObject skinToObject, HairRenderer hairRenderer, GameObject newObject) {
|
|
hairContainer = saveHairContainer(hairContainer, skinToObject);
|
|
if (hairContainer != null) {
|
|
hairRenderer.hairContainer = hairContainer;
|
|
hairRenderer.recreate();
|
|
Selection.activeObject = newObject;
|
|
EditorGUIUtility.PingObject(Selection.activeObject);
|
|
closeWindow = true;
|
|
}
|
|
else {
|
|
startSkinningButton.disableCircle = false;
|
|
DestroyImmediate(newObject);
|
|
}
|
|
}
|
|
|
|
void parentNames(Transform transform, ref List<string> names) {
|
|
names.Insert(0, transform.name);
|
|
if (transform.parent != null) {
|
|
parentNames(transform.parent, ref names);
|
|
}
|
|
}
|
|
|
|
private GUIStyle createTextHeadingStyle() {
|
|
var guiStyle = new GUIStyle(EditorStyles.label) {
|
|
fontStyle = FontStyle.Bold,
|
|
wordWrap = true
|
|
};
|
|
return guiStyle;
|
|
}
|
|
|
|
private void OnDisable() {
|
|
destroyResources();
|
|
}
|
|
|
|
private void destroyResources() {
|
|
if (combinedObject != null) DestroyImmediate(combinedObject);
|
|
if (!(!ReferenceEquals(sourceEditor, null) && sourceEditor == null)) DestroyImmediate(sourceEditor);
|
|
if (helper != null) DestroyImmediate(helper);
|
|
seriaLizedObject = null;
|
|
combinedObject = null;
|
|
sourceEditor = null;
|
|
helper = null;
|
|
|
|
sourceGamObject = null;
|
|
alembicGroom = null;
|
|
dontSkin = null;
|
|
}
|
|
|
|
private void OnEnable() {
|
|
initialize();
|
|
}
|
|
|
|
private void initialize() {
|
|
helper = new GameObject();
|
|
seriaLizedObject = new SerializedObject(helper.AddComponent<AlembicSetupHelper>());
|
|
helper.hideFlags = HideFlags.HideInHierarchy;
|
|
sourceGamObject = seriaLizedObject.FindProperty("sourceGameObject");
|
|
alembicGroom = seriaLizedObject.FindProperty("alembicFile");
|
|
dontSkin = seriaLizedObject.FindProperty("dontSkin");
|
|
startSkinningButton = new ImportantButton() {
|
|
positionRect = new Rect(),
|
|
resource = "build_hairs",
|
|
gradientResource = "rate_button_gradient",
|
|
disableCircleAfterClick = true,
|
|
clickAction = skinAndCreateObject
|
|
};
|
|
renderersInSourceMesh = new List<Renderer>();
|
|
}
|
|
|
|
|
|
private void OnGUI() {
|
|
#if UNITY_EDITOR
|
|
if (BuildPipeline.isBuildingPlayer) return;
|
|
if (seriaLizedObject?.targetObject == null) {
|
|
recreate();
|
|
}
|
|
#endif
|
|
if (closeWindow) {
|
|
Close();
|
|
}
|
|
else {
|
|
GUI.color = Color.black;
|
|
GUI.DrawTexture(new Rect(0, 0, position.width, position.height), EditorGUIUtility.whiteTexture);
|
|
GUI.color = Color.white;
|
|
selectionLockedMessage.fixedColorIndex = 4;
|
|
selectionLockedMessage.drawMessage(position.width);
|
|
scroll = GUILayout.BeginScrollView(scroll, false, false);
|
|
EditorGUILayout.BeginVertical();
|
|
GUILayout.Space(15);
|
|
createStyles();
|
|
EditorGUILayout.BeginVertical(panelStyle);
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUILayout.LabelField("Please assign the Alembic .abc file that contains the hairs(splines) and " +
|
|
"assign the source GameObject that the hairs should be skinned to." +
|
|
" The source GameObject should be the same that was used when grooming " +
|
|
"the alembic file in the third party app and should contain a MeshRenderer/SkinnedMeshRenderer " +
|
|
"that will be used for skinning.", headingStyle);
|
|
GUILayout.Space(17);
|
|
EditorGUILayout.PropertyField(alembicGroom);
|
|
if (!dontSkin.boolValue) {
|
|
EditorGUILayout.PropertyField(sourceGamObject);
|
|
}
|
|
|
|
EditorGUILayout.PropertyField(dontSkin,
|
|
new GUIContent("Don't skin to a Mesh",
|
|
"Use this option when you don't need the hair to be skinned to a source mesh. For instance human hair often doesn't need to be skinned to a mesh."))
|
|
;
|
|
if (GUI.changed) {
|
|
isInErrorState = errorCheckFields();
|
|
createPreview();
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
drawStartSkinningButton();
|
|
drawBetaImage();
|
|
drawMeshWrongStateButton();
|
|
seriaLizedObject?.ApplyModifiedProperties();
|
|
EditorGUILayout.EndVertical();
|
|
GUILayout.EndScrollView();
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
private void recreate() {
|
|
destroyResources();
|
|
initialize();
|
|
}
|
|
|
|
private void drawMeshWrongStateButton() {
|
|
if (isMeshInWrongFormat) {
|
|
if (AddFurCreatorUI.draw32IndexFormatWarning(errorMesh, panelStyle, headingStyle, buttonStyle)) {
|
|
createSourceAndAlembicPreviewObject();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool isInErrorState;
|
|
|
|
private bool errorCheckFields() {
|
|
if (sourceGamObject.objectReferenceValue != null) {
|
|
var renderers = (sourceGamObject.objectReferenceValue as GameObject)?.GetComponentsInChildren<Renderer>();
|
|
if (renderers == null || renderers.Length == 0) {
|
|
showToastMessage("The Source GameObject does not contain any renderers.");
|
|
return true;
|
|
}
|
|
|
|
var sourceObjectAlembicCurves = (sourceGamObject.objectReferenceValue as GameObject)?.GetComponentsInChildren<AlembicStreamPlayer>();
|
|
if (sourceObjectAlembicCurves != null && sourceObjectAlembicCurves.Length > 0) {
|
|
showToastMessage("The Alembic file should be assigned in the field above :)");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var alembicCurves = (alembicGroom.objectReferenceValue as AlembicStreamPlayer)?.gameObject.GetComponentsInChildren<AlembicCurves>();
|
|
if (alembicGroom.objectReferenceValue != null && alembicCurves == null || alembicCurves?.Length == 0) {
|
|
showToastMessage("The assigned Alembic file does not contain an AlembicCurves.");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void showToastMessage(string me) {
|
|
selectionLockedMessage.show = true;
|
|
selectionLockedMessage.messageText = me;
|
|
selectionLockedMessage.autoHide();
|
|
}
|
|
|
|
private void drawBetaImage() {
|
|
if (shouldNotDrawPreview()) {
|
|
betaTexture ??= Resources.Load<Texture2D>("beta_img");
|
|
GUI.DrawTexture(new Rect(0, position.height - 355, position.width, 192), betaTexture);
|
|
}
|
|
}
|
|
|
|
private Texture2D betaTexture;
|
|
|
|
private void drawStartSkinningButton() {
|
|
if (sourceEditor != null && !isInErrorState && !isMeshInWrongFormat) {
|
|
editorDeltaTime.Update();
|
|
startSkinningButton.update(editorDeltaTime.deltaTime);
|
|
if (renderersInSourceMesh.Count > 1) {
|
|
drawWithMultipleRenderers();
|
|
}
|
|
else {
|
|
drawWithOnlyOneRendererFound();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void drawWithMultipleRenderers() {
|
|
if (shouldNotDrawPreview()) return;
|
|
EditorGUILayout.BeginVertical(panelStyle, GUILayout.Width(200), GUILayout.Height(300));
|
|
EditorGUILayout.LabelField("Please select which object\nthe hairs should be skinned to.", headingStyle,
|
|
GUILayout.Width(200), GUILayout.Height(30));
|
|
EditorGUILayout.Space(15);
|
|
for (var index = 0; index < renderersInSourceMesh.Count; index++) {
|
|
var renderer = renderersInSourceMesh[index];
|
|
if (GUILayout.Button(renderer.gameObject.name, selectedRendererIndex == index ? buttonStyleSelected : buttonStyle)) {
|
|
selectedRendererIndex = index;
|
|
}
|
|
|
|
EditorGUILayout.Space(3);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
var previewSize = new Rect(245, 186, 590 - 241, 296);
|
|
sourceEditor.DrawPreview(previewSize);
|
|
if (shouldDrawSkinningButton()) {
|
|
startSkinningButton.positionRect = createBuildButtonRect(122);
|
|
startSkinningButton.draw();
|
|
previewSize.y += 277;
|
|
drawCheckScaleAndLayoutText(previewSize, true);
|
|
textStyle.alignment = TextAnchor.MiddleLeft;
|
|
}
|
|
|
|
if (Event.current.type == EventType.Repaint) {
|
|
var vignetteAndOutlineRect = new Rect(242, 185, 590 - 235, 301);
|
|
vignetteStyle.Draw(vignetteAndOutlineRect, false, true, false, false);
|
|
panelOutlineStyle.Draw(vignetteAndOutlineRect, false, true, false, false);
|
|
}
|
|
}
|
|
|
|
private bool shouldNotDrawPreview() {
|
|
return sourceEditor == null || alembicGroom.objectReferenceValue == null && dontSkin.boolValue;
|
|
}
|
|
|
|
private void drawWithOnlyOneRendererFound() {
|
|
if (shouldNotDrawPreview()) return;
|
|
var previewSize = new Rect(7, 186, 588, 297);
|
|
sourceEditor.DrawPreview(previewSize);
|
|
if (shouldDrawSkinningButton()) {
|
|
startSkinningButton.positionRect = createBuildButtonRect(0);
|
|
startSkinningButton.draw();
|
|
previewSize.y += 277;
|
|
drawCheckScaleAndLayoutText(previewSize, false);
|
|
}
|
|
|
|
if (Event.current.type == EventType.Repaint) {
|
|
var vignetteAndOutlineRect = new Rect(4, 185, 594, 301);
|
|
vignetteStyle.Draw(vignetteAndOutlineRect, false, true, false, false);
|
|
panelOutlineStyle.Draw(vignetteAndOutlineRect, false, true, false, false);
|
|
}
|
|
}
|
|
|
|
private void drawCheckScaleAndLayoutText(Rect previewSize, bool isNarrovLayout) {
|
|
textStyle ??= EditorStyles.label;
|
|
textStyle.alignment = TextAnchor.MiddleCenter;
|
|
textStyle.normal.textColor = Color.white;
|
|
if (isNarrovLayout) {
|
|
GUI.Label(previewSize,
|
|
"Please check that the objects are aligned\nproperly and make sure the scaling looks\ncorrect in the preview. You can adjust the\nScale Factor in the Model Import Settings.",
|
|
textStyle);
|
|
}
|
|
else {
|
|
GUI.Label(previewSize,
|
|
"Please check that the objects are aligned properly\nand make sure the scaling looks correct in the preview.\nYou can adjust the Scale Factor in the Model Import Settings.",
|
|
textStyle);
|
|
}
|
|
|
|
textStyle.alignment = TextAnchor.MiddleLeft;
|
|
}
|
|
|
|
private bool shouldDrawSkinningButton() {
|
|
return sourceGamObject.objectReferenceValue != null && alembicGroom.objectReferenceValue != null ||
|
|
alembicGroom.objectReferenceValue != null && dontSkin.boolValue;
|
|
}
|
|
|
|
private Rect createBuildButtonRect(float xOffset) {
|
|
return new Rect(
|
|
position.width / 2f - BUILD_BUTTON_WIDTH / 2f + xOffset,
|
|
515f,
|
|
BUILD_BUTTON_WIDTH,
|
|
BUILD_BUTTON_HEIGHT
|
|
);
|
|
}
|
|
|
|
private void createStyles() {
|
|
headingStyle ??= createTextHeadingStyle();
|
|
panelStyle ??= BrushPropertiesUI.createDefaultPanelStyle();
|
|
panelOutlineStyle ??= BrushPropertiesUI.createDefaultPanelStyle("bg_box_pink");
|
|
vignetteStyle ??= BrushPropertiesUI.createDefaultPanelStyle("vignette_tex");
|
|
buttonStyle ??= PainterLayersUI.createButtonStyle("bg_box", "bg_box");
|
|
buttonStyle.padding = new RectOffset(28, 16, 8, 10);
|
|
buttonStyleSelected ??= PainterLayersUI.createButtonStyle("bg_box_blue", "bg_box_blue");
|
|
buttonStyleSelected.padding = new RectOffset(28, 16, 8, 10);
|
|
}
|
|
|
|
private void createPreview() {
|
|
if (combinedObject != null) DestroyImmediate(combinedObject);
|
|
if (sourceEditor != null) DestroyImmediate(sourceEditor);
|
|
if (alembicGroom.objectReferenceValue != null ||
|
|
sourceGamObject.objectReferenceValue != null && !isInErrorState && !isMeshInWrongFormat) {
|
|
combinedObject = createSourceAndAlembicPreviewObject();
|
|
sourceEditor = Editor.CreateEditor(combinedObject);
|
|
}
|
|
}
|
|
|
|
private GameObject createSourceAndAlembicPreviewObject() {
|
|
GameObject co = new GameObject();
|
|
co.hideFlags = HideFlags.HideAndDontSave;
|
|
if (sourceGamObject.objectReferenceValue != null && !dontSkin.boolValue) {
|
|
var sourceGameObject = Instantiate((GameObject) sourceGamObject.objectReferenceValue, co.transform, true);
|
|
sourceGameObject.hideFlags = HideFlags.HideAndDontSave;
|
|
selectedRendererIndex = 0;
|
|
renderersInSourceMesh.Clear();
|
|
var meshRenderers = sourceGamObject.objectReferenceValue.GetComponentsInChildren<MeshRenderer>();
|
|
renderersInSourceMesh.AddRange(meshRenderers);
|
|
var skinnedMeshRenderers = sourceGamObject.objectReferenceValue.GetComponentsInChildren<SkinnedMeshRenderer>();
|
|
renderersInSourceMesh.AddRange(skinnedMeshRenderers);
|
|
var mesh = meshRenderers.Length > 0 ? meshRenderers[0].GetComponent<MeshFilter>().sharedMesh : skinnedMeshRenderers[0].sharedMesh;
|
|
isMeshInWrongFormat = mesh == null || AddFurCreatorUI.isMedUnreadableOrIndexFormat16(mesh);
|
|
errorMesh = isMeshInWrongFormat ? mesh : null;
|
|
}
|
|
|
|
if (alembicGroom.objectReferenceValue != null) {
|
|
var alembicPreview = Instantiate(((AlembicStreamPlayer) alembicGroom.objectReferenceValue).gameObject, co.transform, true);
|
|
alembicPreview.hideFlags = HideFlags.HideAndDontSave;
|
|
ensureCurveRenderer(alembicPreview);
|
|
}
|
|
|
|
//If your camera is ever places on this position and you accidentally see the preview object in your scene.
|
|
//Please send me a screenshot to daniel@danielzeller.no and let me know you've unlocked an easter egg.
|
|
co.transform.position = Vector3.left * 10000000000000000000;
|
|
|
|
return co;
|
|
}
|
|
|
|
private void ensureCurveRenderer(GameObject alembicPreview) {
|
|
var curves = alembicPreview.GetComponentsInChildren<AlembicCurves>();
|
|
foreach (var curve in curves) {
|
|
var renderer = curve.GetComponent<AlembicCurvesRenderer>();
|
|
if (renderer == null) {
|
|
renderer = curve.AddComponent<AlembicCurvesRenderer>();
|
|
}
|
|
|
|
var meshRenderer = renderer.GetComponent<MeshRenderer>();
|
|
if (meshRenderer != null && renderersInSourceMesh.Count > 0) {
|
|
Material material = null;
|
|
Material mvm = null;
|
|
DefaultMaterialLoader.loadDefaultMaterial(out bool _, out bool _, ref material, out Material _, ref mvm,
|
|
"Strands", renderersInSourceMesh[selectedRendererIndex]);
|
|
meshRenderer.material = material;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static HairContainer saveHairContainer(HairContainer hairRenderer, string path) {
|
|
#if UNITY_EDITOR
|
|
var containerCopy = Instantiate(hairRenderer);
|
|
AssetDatabase.CreateAsset(containerCopy, path);
|
|
Selection.activeObject = containerCopy;
|
|
return containerCopy;
|
|
#endif
|
|
}
|
|
|
|
public static HairContainer saveHairContainer(HairContainer hairContainer, GameObject skinTo) {
|
|
#if UNITY_EDITOR
|
|
|
|
var path = EditorUtility.SaveFilePanel("Save The HairContainer", "Assets/", skinTo.name + "HairContainer", "asset");
|
|
if (!string.IsNullOrEmpty(path)) {
|
|
hairContainer.regenerateID();
|
|
path = FileUtil.GetProjectRelativePath(path);
|
|
|
|
var existingFurContainer = AssetDatabase.LoadAssetAtPath<HairContainer>(path);
|
|
if (existingFurContainer != null && existingFurContainer != hairContainer) {
|
|
AssetDatabase.DeleteAsset(path);
|
|
hairContainer = saveHairContainer(hairContainer, path);
|
|
}
|
|
else if (existingFurContainer == null) {
|
|
hairContainer = saveHairContainer(hairContainer, path);
|
|
}
|
|
|
|
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
EditorUtility.FocusProjectWindow();
|
|
return hairContainer;
|
|
}
|
|
|
|
return null;
|
|
#endif
|
|
}
|
|
}
|
|
} |