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.

356 lines
17 KiB
C#

4 years ago
using System;
using System.Collections.Generic;
using System.Threading;
using Unity.Collections;
using UnityEngine;
namespace FluffyGroomingTool {
public struct PointOnMesh {
public Vector3 pos;
public int triangleIndex1;
public int triangleIndex2;
public int triangleIndex3;
public Vector3 vertex1;
public Vector3 vertex2;
public Vector3 vertex3;
}
public class PointsOnMeshJob {
public bool isCompleted;
private int[] triangles;
private Vector3[] vertices;
public List<PointOnMesh> output;
private List<PointAndIndex>[,,] newPointsOnGrid;
private List<GroomAndPosition>[,,] existingPointsOnGrid;
public float distanceBetweenStrands;
private Vector3 gridResolution;
private Vector3 gridZeroOffsets;
private float gridSize;
public HairStrand[] createdHairStrands;
public StrandGroom[] createdStrandGrooms;
public FurCreator furCreator;
private Vector2[] uvs;
public Mesh mesh;
public Action OnCompleteAction { get; set; }
public StrandGroom[] existingGrooms { get; set; }
public HairStrand[] existingHairStrands { get; set; }
public bool isClumpJob = false;
public float layerIndex;
private NativeArray<Color32> maskTexture;
private Vector2 maskTextureSize;
private int getNumberOfSamples(Vector3 vert1, Vector3 vert2, Vector3 vert3) {
var dist1 = Vector3.Distance(vert1, vert2);
var dist2 = Vector3.Distance(vert2, vert3);
var dist3 = Vector3.Distance(vert3, vert1);
var biggestDistance1And2 = dist1 < dist2 ? dist2 : dist1;
var biggestDistance = biggestDistance1And2 < dist3 ? dist3 : biggestDistance1And2;
var numSamples = biggestDistance / 2f / distanceBetweenStrands;
return (int)Math.Max(numSamples, 2f);
}
private float getClosestDistance(Vector3 vertexCandidate) {
float closestDistance = float.MaxValue;
Vector3 candidateGridCoordinate = new Vector3(
(int)((vertexCandidate.x - gridZeroOffsets.x) * gridResolution.x),
(int)((vertexCandidate.y - gridZeroOffsets.y) * gridResolution.y),
(int)((vertexCandidate.z - gridZeroOffsets.z) * gridResolution.z)
);
var pointsGridLength = gridSize - 1;
var maxX = (int)Math.Clamp(candidateGridCoordinate.x + 1, 0, pointsGridLength);
var maxY = (int)Math.Clamp(candidateGridCoordinate.y + 1, 0, pointsGridLength);
var maxZ = (int)Math.Clamp(candidateGridCoordinate.z + 1, 0, pointsGridLength);
for (int x = (int)Math.Max(candidateGridCoordinate.x - 1, 0); x <= maxX; x++) {
for (int y = (int)Math.Max(candidateGridCoordinate.y - 1, 0); y <= maxY; y++) {
for (int z = (int)Math.Max(candidateGridCoordinate.z - 1, 0); z <= maxZ; z++) {
var pointsInCell = newPointsOnGrid[x, y, z] =
newPointsOnGrid[x, y, z] != null ? newPointsOnGrid[x, y, z] : new List<PointAndIndex>();
for (int i = 0; i < pointsInCell.Count; i++) {
float curDist = Vector3.Distance(vertexCandidate, pointsInCell[i].position);
closestDistance = curDist < closestDistance ? curDist : closestDistance;
}
}
}
}
return closestDistance;
}
public int getClosestClumpGuideIndex(Vector3 vertexCandidate, int neighborCells = 1) {
var closestDistance = float.MaxValue;
var closestClumpIndex = -1;
var candidateGridCoordinate = new Vector3(
(int)((vertexCandidate.x - gridZeroOffsets.x) * gridResolution.x),
(int)((vertexCandidate.y - gridZeroOffsets.y) * gridResolution.y),
(int)((vertexCandidate.z - gridZeroOffsets.z) * gridResolution.z)
);
var pointsGridLength = gridSize - 1;
var maxX = (int)Math.Clamp(candidateGridCoordinate.x + neighborCells, 0, pointsGridLength);
var maxY = (int)Math.Clamp(candidateGridCoordinate.y + neighborCells, 0, pointsGridLength);
var maxZ = (int)Math.Clamp(candidateGridCoordinate.z + neighborCells, 0, pointsGridLength);
for (int x = (int)Math.Max(candidateGridCoordinate.x - neighborCells, 0); x <= maxX; x++) {
for (int y = (int)Math.Max(candidateGridCoordinate.y - neighborCells, 0); y <= maxY; y++) {
for (int z = (int)Math.Max(candidateGridCoordinate.z - neighborCells, 0); z <= maxZ; z++) {
var pointsInCell = newPointsOnGrid[x, y, z] != null ? newPointsOnGrid[x, y, z] : new List<PointAndIndex>();
for (int i = 0; i < pointsInCell.Count; i++) {
var pointAndIndex = pointsInCell[i];
float curDist = Vector3.Distance(vertexCandidate, pointAndIndex.position);
if (curDist < closestDistance) {
closestDistance = curDist;
closestClumpIndex = pointAndIndex.index;
}
}
}
}
}
if (closestClumpIndex == -1 && neighborCells == 4) {
return getClosestClumpGuideIndex(vertexCandidate, newPointsOnGrid.Length);
}
if (closestClumpIndex == -1) {
return getClosestClumpGuideIndex(vertexCandidate, 4);
}
return closestClumpIndex;
}
private StrandGroom getClosesExistingGroom(PointOnMesh pointOnMesh, int neighborCells = 1) {
float closestDistance = float.MaxValue;
StrandGroom closestGroom = null;
Vector3 candidateGridCoordinate = new Vector3(
(int)((pointOnMesh.pos.x - gridZeroOffsets.x) * gridResolution.x),
(int)((pointOnMesh.pos.y - gridZeroOffsets.y) * gridResolution.y),
(int)((pointOnMesh.pos.z - gridZeroOffsets.z) * gridResolution.z)
);
var pointsGridLength = gridSize - 1;
var maxX = (int)Math.Min(candidateGridCoordinate.x + neighborCells, pointsGridLength);
var maxY = (int)Math.Min(candidateGridCoordinate.y + neighborCells, pointsGridLength);
var maxZ = (int)Math.Min(candidateGridCoordinate.z + neighborCells, pointsGridLength);
for (int x = (int)Math.Max(candidateGridCoordinate.x - neighborCells, 0); x <= maxX; x++) {
for (int y = (int)Math.Max(candidateGridCoordinate.y - neighborCells, 0); y <= maxY; y++) {
for (int z = (int)Math.Max(candidateGridCoordinate.z - neighborCells, 0); z <= maxZ; z++) {
var pointsInCell = existingPointsOnGrid[x, y, z] != null ? existingPointsOnGrid[x, y, z] : new List<GroomAndPosition>();
for (int i = 0; i < pointsInCell.Count; i++) {
var pointAndIndex = pointsInCell[i];
float curDist = Vector3.Distance(pointOnMesh.pos, pointAndIndex.position);
if (curDist < closestDistance) {
closestDistance = curDist;
closestGroom = pointAndIndex.groom;
}
}
}
}
}
if (closestGroom == null && neighborCells == 4) {
return getClosesExistingGroom(pointOnMesh, 10);
}
if (closestGroom == null && neighborCells == 1) {
return getClosesExistingGroom(pointOnMesh, 4);
}
if (neighborCells > 4 && closestGroom == null) {
closestGroom = existingGrooms[0];
}
return closestGroom;
}
private void addPoint(PointOnMesh pos) {
var index = output.Count;
output.Add(pos);
var gridResolutionX = (int)Math.Clamp((pos.pos.x - gridZeroOffsets.x) * gridResolution.x, 0, gridSize - 1);
var gridResolutionY = (int)Math.Clamp((pos.pos.y - gridZeroOffsets.y) * gridResolution.y, 0, gridSize - 1);
var gridResolutionZ = (int)Math.Clamp((pos.pos.z - gridZeroOffsets.z) * gridResolution.z, 0, gridSize - 1);
newPointsOnGrid[gridResolutionX, gridResolutionY, gridResolutionZ].Add(new PointAndIndex {
index = index, position = pos.pos
});
}
public class PointAndIndex {
public int index;
public Vector3 position;
}
public class GroomAndPosition {
public StrandGroom groom;
public Vector3 position;
}
private Thread thread;
public void start() {
setupValues();
thread = new Thread(execute);
thread.Start();
}
private void setupValues() {
gridSize = getGridSize(distanceBetweenStrands);
gridResolution = getGridResolution();
var size = (int)gridSize;
newPointsOnGrid = new List<PointAndIndex>[size, size, size];
existingPointsOnGrid = new List<GroomAndPosition>[size, size, size];
vertices = mesh.vertices;
triangles = mesh.triangles;
output = new List<PointOnMesh>();
gridZeroOffsets = mesh.bounds.min;
uvs = mesh.uv;
var tex = furCreator.getActiveLayer().maskTexture;
if (tex != null) {
maskTexture = fixTextureFormat(tex);
maskTextureSize = new Vector2(tex.width, tex.height);
}
}
private NativeArray<Color32> fixTextureFormat(Texture2D tex) {
var texture = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false);
texture.SetPixels(tex.GetPixels());
return texture.GetRawTextureData<Color32>();
}
private void execute() {
addExistingPointsToGrid();
float numberOfIndices = triangles.Length;
float numberOfTriangles = numberOfIndices / 3;
for (int x = 0; x < numberOfTriangles; x++) {
int triID = x * 3;
if (triID < numberOfTriangles * 3) {
PointOnMesh pointOnMesh = new PointOnMesh();
pointOnMesh.triangleIndex1 = triangles[triID + 0];
pointOnMesh.triangleIndex2 = triangles[triID + 1];
pointOnMesh.triangleIndex3 = triangles[triID + 2];
var triMaskedAmount = triangleMaskedAmount(pointOnMesh);
// if (triMaskedAmount == 0) continue; //Entire triangle is masked out.
pointOnMesh.vertex1 = vertices[pointOnMesh.triangleIndex1];
pointOnMesh.vertex2 = vertices[pointOnMesh.triangleIndex2];
pointOnMesh.vertex3 = vertices[pointOnMesh.triangleIndex3];
Vector3 ab = pointOnMesh.vertex2 - pointOnMesh.vertex1;
Vector3 ac = pointOnMesh.vertex3 - pointOnMesh.vertex1;
int samplesPerTriangle = getNumberOfSamples(pointOnMesh.vertex1, pointOnMesh.vertex2, pointOnMesh.vertex3);
for (int i = 0; i < samplesPerTriangle; i++) {
Vector2 randomSeed = new Vector2(
new Vector2(x + i + layerIndex, layerIndex + triID + i * 2).rand(),
new Vector2(triID + i + layerIndex / 2, pointOnMesh.triangleIndex2 * i * 2 + layerIndex / 2).rand()
);
randomSeed = randomSeed.x + randomSeed.y > 1.0 ? Vector2.one - randomSeed : randomSeed;
pointOnMesh.pos = pointOnMesh.vertex1 + randomSeed.x * ab + randomSeed.y * ac;
if (triMaskedAmount < 255 * 3 && isPointMasked(pointOnMesh)) continue;
var closestDistance = getClosestDistance(pointOnMesh.pos);
if (closestDistance > distanceBetweenStrands) {
addPoint(pointOnMesh);
}
}
}
}
var tempHairStrands = new List<HairStrand>();
var tempStrandGroom = new List<StrandGroom>();
foreach (var t in output) {
tempHairStrands.Add(FurCreator.createPointAtPosition(t, uvs));
if (isClumpJob) {
tempStrandGroom.Add(hasExistingGroom() ? (StrandGroom)getClosesExistingGroom(t).Clone() : StrandGroom.zero());
} else {
tempStrandGroom.Add(hasExistingGroom() ? getClosesExistingGroom(t) : StrandGroom.zero());
}
}
createdHairStrands = tempHairStrands.ToArray();
createdStrandGrooms = tempStrandGroom.ToArray();
OnCompleteAction?.Invoke();
isCompleted = true;
if (maskTexture.IsCreated) maskTexture.Dispose();
}
private bool isPointMasked(PointOnMesh pointOnMesh) {
var uv = pointOnMesh.creatBaryCentricMeshCoordinates().interpolatedUv(uvs);
var index = (int)(
(int)Math.Min(maskTextureSize.x - 1, Math.Floor(maskTextureSize.x * uv.x)) +
maskTextureSize.x * Math.Min(maskTextureSize.y - 1, Math.Floor(maskTextureSize.y * uv.y))
);
return maskTexture[index].r < 128;
}
private float triangleMaskedAmount(PointOnMesh pointOnMesh) {
if (!maskTexture.IsCreated || maskTexture.Length == 0) return 256 * 3;
var uv1 = uvs[pointOnMesh.triangleIndex1];
var uv2 = uvs[pointOnMesh.triangleIndex2];
var uv3 = uvs[pointOnMesh.triangleIndex3];
var uv1X = (int)(
(int)Math.Min(maskTextureSize.x - 1, Math.Floor(maskTextureSize.x * uv1.x)) +
maskTextureSize.x * Math.Min(maskTextureSize.y - 1, Math.Floor(maskTextureSize.y * uv1.y))
);
var uv2X = (int)(
(int)Math.Min(maskTextureSize.x - 1, Math.Floor(maskTextureSize.x * uv2.x)) +
maskTextureSize.x * Math.Min(maskTextureSize.y - 1, Math.Floor(maskTextureSize.y * uv2.y))
);
var uv3X = (int)(
(int)Math.Min(maskTextureSize.x - 1, Math.Floor(maskTextureSize.x * uv3.x)) +
maskTextureSize.x * Math.Min(maskTextureSize.y - 1, Math.Floor(maskTextureSize.y * uv3.y))
);
return maskTexture[uv1X].r + maskTexture[uv2X].r + maskTexture[uv3X].r;
}
private void addExistingPointsToGrid() {
if (hasExistingGroom()) {
for (int i = 0; i < existingGrooms.Length; i++) {
StrandGroom groom = existingGrooms[i];
HairStrand existingHairStrand = existingHairStrands[i];
Vector3 position = Barycentric.interpolateV3(
vertices[(int)existingHairStrand.triangles.x],
vertices[(int)existingHairStrand.triangles.y],
vertices[(int)existingHairStrand.triangles.z],
existingHairStrand.barycentricCoordinates
);
var x = (int)Math.Clamp((position.x - gridZeroOffsets.x) * gridResolution.x, 0, gridSize - 1);
var y = (int)Math.Clamp((position.y - gridZeroOffsets.y) * gridResolution.y, 0, gridSize - 1);
var z = (int)Math.Clamp((position.z - gridZeroOffsets.z) * gridResolution.z, 0, gridSize - 1);
try {
existingPointsOnGrid[x, y, z] =
existingPointsOnGrid[x, y, z] != null ? existingPointsOnGrid[x, y, z] : new List<GroomAndPosition>();
existingPointsOnGrid[x, y, z].Add(new GroomAndPosition { position = position, groom = groom });
} catch {
//Do nothing. This can happen if the mesh gets corrupted or contains Nan values.
}
}
}
}
private bool hasExistingGroom() {
return existingGrooms != null && existingGrooms.Length > 0 && existingGrooms.Length == existingHairStrands.Length;
}
public void cancel() {
thread?.Abort();
}
private float getGridSize(float furCreatorDistanceBetweenStrands) {
var biggestSizeXY = mesh.bounds.size.x > mesh.bounds.size.y ? mesh.bounds.size.x : mesh.bounds.size.y;
var biggestSize = biggestSizeXY > mesh.bounds.size.z ? biggestSizeXY : mesh.bounds.size.z;
var gSize = Mathf.Clamp(biggestSize / furCreatorDistanceBetweenStrands, 5, 100);
//Round to nearest ten.
if (gSize % 10 == 0) return gSize;
return (10 - gSize % 10) + gSize;
}
private Vector3 getGridResolution() {
return new Vector3(
gridSize / mesh.bounds.size.x,
gridSize / mesh.bounds.size.y,
gridSize / mesh.bounds.size.z
);
}
}
}