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.

374 lines
17 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace FluffyGroomingTool {
public static class FurCreatorExtensions {
private const string saveGroomText = "Would you also like to save the fur GroomContainer in case you want to make changes later on?" +
" (This will create an asset for the groom, and requires extra disk space)";
private const string saveHeading = "Save The FurContainer";
private const string saveHeadingGroom = "Save The GroomContainer";
public static void finalizeGroomAndSaveFur(this FurCreator f) {
f.permanentlyDeleteMaskedStrands(() => {
#if UNITY_EDITOR
f.clearWindSelection();
var gameObject = f.gameObject;
gameObject.SetActive(false);
var furRenderer = f.FurRenderer;
var path = EditorUtility.SaveFilePanel(saveHeading, "Assets/", gameObject.name + "FurContainer", "asset");
if (!string.IsNullOrEmpty(path)) {
createNewId(furRenderer);
path = FileUtil.GetProjectRelativePath(path);
furRenderer.furContainer.disposeBuffers();
var existingFurContainer = AssetDatabase.LoadAssetAtPath<FurContainer>(path);
if (existingFurContainer != null && existingFurContainer != furRenderer.furContainer) {
AssetDatabase.DeleteAsset(path);
saveFurContainer(furRenderer, path);
}
else if (existingFurContainer == null) {
saveFurContainer(furRenderer, path);
}
if (EditorUtility.DisplayDialog($"{saveHeadingGroom}?", saveGroomText, "Yes", "No")) {
var pathGroom = EditorUtility.SaveFilePanel(saveHeadingGroom,
path.Substring(0, path.LastIndexOf('/')), gameObject.name + "GroomContainer", "asset");
if (!string.IsNullOrEmpty(pathGroom)) {
pathGroom = FileUtil.GetProjectRelativePath(pathGroom);
var existingGroom = AssetDatabase.LoadAssetAtPath<GroomContainer>(pathGroom);
f.groomContainer.disposeBuffers();
if (existingGroom != null && existingGroom != f.groomContainer) {
AssetDatabase.DeleteAsset(pathGroom);
AssetDatabase.CreateAsset(Object.Instantiate(f.groomContainer), pathGroom);
}
else if (existingGroom == null) {
AssetDatabase.CreateAsset(Object.Instantiate(f.groomContainer), pathGroom);
}
furRenderer.furContainer.groomContainerGuid = AssetDatabase.AssetPathToGUID(pathGroom);
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.FocusProjectWindow();
furRenderer.CurrentRenderer = null;
gameObject.SetActive(true);
furRenderer.furContainer.recreateAll.Invoke();
Object.DestroyImmediate(f);
}
gameObject.SetActive(true);
#endif
});
}
private static void clearWindSelection(this FurCreator f) {
if (f.getPainterProperties().type == (int) PaintType.WIND_MAX_DISTANCE) {
f.getPainterProperties().type = (int) PaintType.HEIGHT;
f.groomContainer.needsUpdate = true;
f.groomContainer.update();
f.FurRenderer.drawWindContribution = false;
}
}
private static void createNewId(FurRenderer furRenderer) {
furRenderer.furContainer.regenerateID();
furRenderer.currentFurContainerID = furRenderer.furContainer.id;
}
private static void saveFurContainer(FurRenderer furRenderer, string path) {
#if UNITY_EDITOR
var containerCopy = Object.Instantiate(furRenderer.furContainer);
AssetDatabase.CreateAsset(containerCopy, path);
furRenderer.furContainer = containerCopy;
Selection.activeObject = containerCopy;
#endif
}
}
public class FurMeshCreator {
private IEnumerator meshCoroutine;
private Stopwatch sw;
private const string saveMeshAsset = "Save Mesh Asset";
public void createMesh(FurCreator furCreator) {
#if UNITY_EDITOR
sw = Stopwatch.StartNew();
var path = EditorUtility.SaveFilePanel(saveMeshAsset, "Assets/", furCreator.name + "FurMesh", "asset");
if (!string.IsNullOrEmpty(path)) {
var newMesh = createProceduralMesh(furCreator);
Undo.RecordObject(furCreator.gameObject, "Undo");
newMesh.RecalculateBounds();
newMesh.RecalculateTangents();
newMesh.boneWeights = createBoneWeights(furCreator, newMesh);
newMesh.bindposes = furCreator.FurRenderer.meshBaker.sourceMesh.bindposes;
furCreator.enabled = false;
furCreator.FurRenderer.enabled = false;
furCreator.FurRenderer.meshBaker.sourceMesh = null;
saveMeshAndCreateNewGameObject(furCreator.gameObject, newMesh, path, furCreator.FurRenderer.material);
}
#endif
}
private static BoneWeight[] createBoneWeights(FurCreator furCreator, Mesh newMesh) {
var sourceMeshVertices = furCreator.FurRenderer.meshBaker.sourceMesh.vertices;
var sourceMeshBoneWeights = furCreator.FurRenderer.meshBaker.sourceMesh.boneWeights;
var newMeshVertices = newMesh.vertices;
var newMeshBoneWeights = new List<BoneWeight>();
var layerVertexStartIndex = 0;
foreach (var layer in furCreator.FurRenderer.furContainer.layerStrandsList) {
var layerVerticesCount = layer.hairStrandsBuffer.count * layer.CardMesh.vertexCount;
for (var i = 0; i < layerVerticesCount; i++) {
var proceduralMeshIndex = layerVertexStartIndex + i;
var cardMeshVertexCount = layer.CardMesh.vertexCount;
var hairStrandIndex = (int) Math.Floor((float) i / cardMeshVertexCount);
var vertexPos = newMeshVertices[proceduralMeshIndex];
if (sourceMeshBoneWeights.Length > 0) {
newMeshBoneWeights.Add(cetClosestBoneWeight(layer, hairStrandIndex, sourceMeshVertices, sourceMeshBoneWeights, vertexPos));
}
}
layerVertexStartIndex += layerVerticesCount;
}
return newMeshBoneWeights.ToArray();
}
private static BoneWeight cetClosestBoneWeight(
HairStrandLayer layer,
int hairStrandIndex,
Vector3[] sourceMeshVertices,
BoneWeight[] sourceMeshBoneWeights,
Vector3 vertex
) {
var barycentricTriangleIndex = new[] {
(int) layer.layerHairStrands[hairStrandIndex].triangles.x,
(int) layer.layerHairStrands[hairStrandIndex].triangles.y,
(int) layer.layerHairStrands[hairStrandIndex].triangles.z
};
var barycentricVertsPos = new[] {
sourceMeshVertices[barycentricTriangleIndex[0]],
sourceMeshVertices[barycentricTriangleIndex[1]],
sourceMeshVertices[barycentricTriangleIndex[2]]
};
var minDistance = float.MaxValue;
var closestBoneWeight = sourceMeshBoneWeights[barycentricTriangleIndex[0]];
for (var j = 0; j < 3; j++) {
var distance = Vector3.Distance(vertex, barycentricVertsPos[j]);
if (distance < minDistance) {
minDistance = distance;
closestBoneWeight = sourceMeshBoneWeights[barycentricTriangleIndex[j]];
}
}
return closestBoneWeight;
}
struct MeshBufferData {
public Vector3 vertices;
public Vector3 normals;
public Vector4 tangents;
public Vector4 colors;
public Vector2 uv1;
public Vector2 uv2;
public Vector2 uv3;
public Vector2 uv4;
}
private static Mesh createProceduralMesh(FurCreator furCreator) {
var newMesh = new Mesh {indexFormat = IndexFormat.UInt32};
furCreator.FurRenderer.IsCreateMeshPass = true;
furCreator.FurRenderer.furRendererSettings.IsCreateMeshPass = true;
furCreator.FurRenderer.updateAndRenderFur();
furCreator.FurRenderer.IsCreateMeshPass = false;
furCreator.FurRenderer.furRendererSettings.IsCreateMeshPass = false;
//This is kind of dump, but since the C# side of the mesh knows nothing of our mesh buffer modifications, we need this extra step.
var furRendererFurMesh = furCreator.FurRenderer.renderersController.hairMesh;
var furMeshVertexCount = furRendererFurMesh.vertexCount;
var copiedData = new ComputeBuffer(furMeshVertexCount, Marshal.SizeOf<MeshBufferData>());
var cs = Resources.Load<ComputeShader>("MeshBufferToNativeCode");
var vertexBufferStride = furRendererFurMesh.GetVertexBufferStride(0);
cs.SetInt("vertexBufferStride", vertexBufferStride);
cs.SetBuffer(0, "sourceMeshData", furCreator.FurRenderer.renderersController.hairMeshBuffer);
cs.SetBuffer(0, "copiedData", copiedData);
cs.Dispatch(0, furMeshVertexCount.toCsGroups(), 1, 1);
var data = new MeshBufferData[furMeshVertexCount];
copiedData.GetData(data);
newMesh.vertices = data.Select(it => it.vertices).ToArray();
newMesh.normals = data.Select(it => it.normals).ToArray();
newMesh.tangents = data.Select(it => it.tangents).ToArray();
newMesh.colors = data.Select(it => new Color(it.colors.x, it.colors.y, it.colors.z, it.colors.w)).ToArray();
newMesh.uv = data.Select(it => it.uv1).ToArray();
newMesh.uv2 = data.Select(it => it.uv2).ToArray();
newMesh.uv3 = data.Select(it => it.uv3).ToArray();
newMesh.uv4 = data.Select(it => it.uv4).ToArray();
var newMeshTriangles = furRendererFurMesh.triangles;
newMesh.triangles = newMeshTriangles;
copiedData.Dispose();
return newMesh;
}
private void saveMeshAndCreateNewGameObject(GameObject gameObject, Mesh newMesh, string path, Material finalMaterial) {
#if UNITY_EDITOR
path = FileUtil.GetProjectRelativePath(path);
AssetDatabase.CreateAsset(newMesh, path);
AssetDatabase.SaveAssets();
newMesh = (Mesh) AssetDatabase.LoadAssetAtPath(path, typeof(Mesh));
var furObjectName = getFurObjectName(gameObject);
var parent = gameObject.transform.parent;
var existingObject = PainterUtils.findExistingFurObject(furObjectName, parent);
var furObject = existingObject ? existingObject.gameObject : new GameObject();
setParentAndTransform(gameObject, parent, furObject);
furObject.name = furObjectName;
furObject.SetActive(true);
var sourceSkinnedMeshRenderer = gameObject.GetComponent<SkinnedMeshRenderer>();
if (sourceSkinnedMeshRenderer != null) {
var existingSmr = furObject.GetComponent<SkinnedMeshRenderer>();
var skinnedMeshRenderer = existingSmr ? existingSmr : furObject.AddComponent<SkinnedMeshRenderer>();
skinnedMeshRenderer.sharedMesh = newMesh;
skinnedMeshRenderer.rootBone = sourceSkinnedMeshRenderer.rootBone;
skinnedMeshRenderer.bones = sourceSkinnedMeshRenderer.bones;
skinnedMeshRenderer.material = finalMaterial;
}
else {
var existingMf = furObject.GetComponent<MeshFilter>();
MeshFilter mf = existingMf ? existingMf : furObject.AddComponent<MeshFilter>();
mf.sharedMesh = newMesh;
var existingMr = furObject.GetComponent<MeshRenderer>();
MeshRenderer mr = existingMr ? existingMr : furObject.AddComponent<MeshRenderer>();
mr.material = finalMaterial;
}
sw.Stop();
Debug.Log("Created mesh in: " + sw.ElapsedMilliseconds / 1000 + " seconds");
#endif
}
private static void setParentAndTransform(GameObject gameObject, Transform parent, GameObject furObject) {
if (parent) {
furObject.transform.parent = parent;
furObject.transform.position = gameObject.transform.position;
furObject.transform.rotation = gameObject.transform.rotation;
furObject.transform.localScale = gameObject.transform.localScale;
}
else {
furObject.transform.parent = gameObject.transform;
furObject.transform.localPosition = Vector3.zero;
furObject.transform.localRotation = Quaternion.identity;
furObject.transform.localScale = Vector3.one;
}
}
public static string getFurObjectName(GameObject gameObject) {
return gameObject.name + "Fur";
}
private static float CARD_MESH_HEIGHT = 0.12f;
private static float CARD_MESH_WIDTH = 0.19f;
public static Mesh CreateHairCard(CardMeshProperties cardMeshProperties) {
var mesh = new Mesh();
mesh.name = "Fur Card";
var vertices = new Vector3[(cardMeshProperties.cardSubdivisionsX + 1) * (cardMeshProperties.cardSubdivisionsY + 1)];
Vector2[] uv = new Vector2[vertices.Length];
Vector2[] uv2 = new Vector2[vertices.Length];
Color[] colors = new Color[vertices.Length];
for (int i = 0, y = 0; y <= cardMeshProperties.cardSubdivisionsY; y++) {
for (int x = 0; x <= cardMeshProperties.cardSubdivisionsX; x++, i++) {
var yPercent = (float) y / cardMeshProperties.cardSubdivisionsY;
var xPercent = (float) x / cardMeshProperties.cardSubdivisionsX;
float widthMultiplier = getWidthMultiplier(yPercent, cardMeshProperties);
vertices[i] = new Vector3(0, CARD_MESH_HEIGHT * yPercent,
(-CARD_MESH_WIDTH / 2f + CARD_MESH_WIDTH * xPercent) * widthMultiplier);
var moveAbility = y == 0 ? 0 : cardMeshProperties.moveCurve.Evaluate(yPercent);
colors[i] = new Color(moveAbility, y, 0, 1);
uv[i] = new Vector2(xPercent / 2f, yPercent / 2f);
uv2[i] = new Vector2(xPercent, yPercent);
}
}
mesh.vertices = vertices;
mesh.uv = uv;
mesh.uv2 = uv2;
int[] triangles = new int[cardMeshProperties.cardSubdivisionsX * cardMeshProperties.cardSubdivisionsY * 6];
for (int ti = 0, vi = 0, y = 0; y < cardMeshProperties.cardSubdivisionsY; y++, vi++) {
for (int x = 0; x < cardMeshProperties.cardSubdivisionsX; x++, ti += 6, vi++) {
triangles[ti] = vi;
triangles[ti + 3] = triangles[ti + 2] = vi + 1;
triangles[ti + 4] = triangles[ti + 1] = vi + cardMeshProperties.cardSubdivisionsX + 1;
triangles[ti + 5] = vi + cardMeshProperties.cardSubdivisionsX + 2;
}
}
mesh.colors = colors;
mesh.indexFormat = IndexFormat.UInt32;
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateTangents();
mesh.RecalculateBounds();
return mesh;
}
public static int[] generateTriangleIndicesForStrands(int strandPoints, int strandsCount) {
var cardSubdivisionsY = strandPoints - 1;
var triangleIndicesCount = cardSubdivisionsY * 6 * strandsCount;
var allTriangles = new int[triangleIndicesCount];
for (int i = 0; i < cardSubdivisionsY * strandsCount; i++) {
var triStartIndex = i * 6;
int offset = (int) Mathf.Floor((float) i / cardSubdivisionsY);
var vStartIndex = i * 2 + offset * 2;
allTriangles[triStartIndex] = vStartIndex;
allTriangles[triStartIndex + 1] = vStartIndex + 2;
allTriangles[triStartIndex + 2] = vStartIndex + 1;
allTriangles[triStartIndex + 3] = vStartIndex + 2;
allTriangles[triStartIndex + 4] = vStartIndex + 3;
allTriangles[triStartIndex + 5] = vStartIndex + 1;
}
return allTriangles;
}
private static float getWidthMultiplier(float yPercent, CardMeshProperties cardMeshProperties) {
return cardMeshProperties.shapeCurve.Evaluate(yPercent);
}
}
}