// Copyright (C) 2023 Nicholas Maltbie
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// 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.
using UnityEngine;
namespace nickmaltbie.OpenKCC.Utils
{
///
/// Inspired by a post from Jeremy Behreandt from
/// https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db
///
public static class CapsuleMaker
{
///
/// UV Profile for capsule.
///
public enum UvProfile : int
{
Fixed = 0,
Aspect = 1,
Uniform = 2
}
///
/// Create a capsule mesh based on some configuration.
///
///
///
///
///
///
///
///
public static Mesh CapsuleData(
int longitudes = 32,
int latitudes = 16,
int rings = 0,
float depth = 1.0f,
float radius = 0.5f,
UvProfile profile = UvProfile.Aspect)
{
bool calcMiddle = rings > 0;
int halfLats = latitudes / 2;
int halfLatsn1 = halfLats - 1;
int halfLatsn2 = halfLats - 2;
int ringsp1 = rings + 1;
int lonsp1 = longitudes + 1;
float halfDepth = depth * 0.5f;
float summit = halfDepth + radius;
// Vertex index offsets.
int vertOffsetNorthHemi = longitudes;
int vertOffsetNorthEquator = vertOffsetNorthHemi + lonsp1 * halfLatsn1;
int vertOffsetCylinder = vertOffsetNorthEquator + lonsp1;
int vertOffsetSouthEquator = calcMiddle ?
vertOffsetCylinder + lonsp1 * rings :
vertOffsetCylinder;
int vertOffsetSouthHemi = vertOffsetSouthEquator + lonsp1;
int vertOffsetSouthPolar = vertOffsetSouthHemi + lonsp1 * halfLatsn2;
int vertOffsetSouthCap = vertOffsetSouthPolar + lonsp1;
// Initialize arrays.
int vertLen = vertOffsetSouthCap + longitudes;
var vs = new Vector3[vertLen];
var vts = new Vector2[vertLen];
var vns = new Vector3[vertLen];
float toTheta = 2.0f * Mathf.PI / longitudes;
float toPhi = Mathf.PI / latitudes;
float toTexHorizontal = 1.0f / longitudes;
float toTexVertical = 1.0f / halfLats;
// Calculate positions for texture coordinates vertical.
float vtAspectRatio;
switch (profile)
{
case UvProfile.Aspect:
vtAspectRatio = radius / (depth + radius + radius);
break;
case UvProfile.Uniform:
vtAspectRatio = (float)halfLats / (ringsp1 + latitudes);
break;
case UvProfile.Fixed:
default:
vtAspectRatio = 1.0f / 3.0f;
break;
}
float vtAspectNorth = 1.0f - vtAspectRatio;
float vtAspectSouth = vtAspectRatio;
var thetaCartesian = new Vector2[longitudes];
var rhoThetaCartesian = new Vector2[longitudes];
float[] sTextureCache = new float[lonsp1];
// Polar vertices.
for (int j = 0; j < longitudes; ++j)
{
float jf = j;
float sTexturePolar = 1.0f - ((jf + 0.5f) * toTexHorizontal);
float theta = jf * toTheta;
float cosTheta = Mathf.Cos(theta);
float sinTheta = Mathf.Sin(theta);
thetaCartesian[j] = new Vector2(cosTheta, sinTheta);
rhoThetaCartesian[j] = new Vector2(
radius * cosTheta,
radius * sinTheta);
// North.
vs[j] = new Vector3(0.0f, summit, 0.0f);
vts[j] = new Vector2(sTexturePolar, 1.0f);
vns[j] = new Vector3(0.0f, 1.0f, 0f);
// South.
int idx = vertOffsetSouthCap + j;
vs[idx] = new Vector3(0.0f, -summit, 0.0f);
vts[idx] = new Vector2(sTexturePolar, 0.0f);
vns[idx] = new Vector3(0.0f, -1.0f, 0.0f);
}
// Equatorial vertices.
for (int j = 0; j < lonsp1; ++j)
{
float sTexture = 1.0f - j * toTexHorizontal;
sTextureCache[j] = sTexture;
// Wrap to first element upon reaching last.
int jMod = j % longitudes;
Vector2 tc = thetaCartesian[jMod];
Vector2 rtc = rhoThetaCartesian[jMod];
// North equator.
int idxn = vertOffsetNorthEquator + j;
vs[idxn] = new Vector3(rtc.x, halfDepth, -rtc.y);
vts[idxn] = new Vector2(sTexture, vtAspectNorth);
vns[idxn] = new Vector3(tc.x, 0.0f, -tc.y);
// South equator.
int idxs = vertOffsetSouthEquator + j;
vs[idxs] = new Vector3(rtc.x, -halfDepth, -rtc.y);
vts[idxs] = new Vector2(sTexture, vtAspectSouth);
vns[idxs] = new Vector3(tc.x, 0.0f, -tc.y);
}
// Hemisphere vertices.
for (int i = 0; i < halfLatsn1; ++i)
{
float ip1f = i + 1.0f;
float phi = ip1f * toPhi;
// For coordinates.
float cosPhiSouth = Mathf.Cos(phi);
float sinPhiSouth = Mathf.Sin(phi);
// Symmetrical hemispheres mean cosine and sine only needs
// to be calculated once.
float cosPhiNorth = sinPhiSouth;
float sinPhiNorth = -cosPhiSouth;
float rhoCosPhiNorth = radius * cosPhiNorth;
float rhoSinPhiNorth = radius * sinPhiNorth;
float zOffsetNorth = halfDepth - rhoSinPhiNorth;
float rhoCosPhiSouth = radius * cosPhiSouth;
float rhoSinPhiSouth = radius * sinPhiSouth;
float zOffsetSouth = -halfDepth - rhoSinPhiSouth;
// For texture coordinates.
float tTexFac = ip1f * toTexVertical;
float cmplTexFac = 1.0f - tTexFac;
float tTexNorth = cmplTexFac + vtAspectNorth * tTexFac;
float tTexSouth = cmplTexFac * vtAspectSouth;
int iLonsp1 = i * lonsp1;
int vertCurrLatNorth = vertOffsetNorthHemi + iLonsp1;
int vertCurrLatSouth = vertOffsetSouthHemi + iLonsp1;
for (int j = 0; j < lonsp1; ++j)
{
int jMod = j % longitudes;
float sTexture = sTextureCache[j];
Vector2 tc = thetaCartesian[jMod];
// North hemisphere.
int idxn = vertCurrLatNorth + j;
vs[idxn] = new Vector3(
rhoCosPhiNorth * tc.x,
zOffsetNorth,
-rhoCosPhiNorth * tc.y);
vts[idxn] = new Vector2(sTexture, tTexNorth);
vns[idxn] = new Vector3(
cosPhiNorth * tc.x,
-sinPhiNorth,
-cosPhiNorth * tc.y);
// South hemisphere.
int idxs = vertCurrLatSouth + j;
vs[idxs] = new Vector3(
rhoCosPhiSouth * tc.x,
zOffsetSouth,
-rhoCosPhiSouth * tc.y);
vts[idxs] = new Vector2(sTexture, tTexSouth);
vns[idxs] = new Vector3(
cosPhiSouth * tc.x,
-sinPhiSouth,
-cosPhiSouth * tc.y);
}
}
// Cylinder vertices.
if (calcMiddle)
{
// Exclude both origin and destination edges
// (North and South equators) from the interpolation.
float toFac = 1.0f / ringsp1;
int idxCylLat = vertOffsetCylinder;
for (int h = 1; h < ringsp1; ++h)
{
float fac = h * toFac;
float cmplFac = 1.0f - fac;
float tTexture = cmplFac * vtAspectNorth + fac * vtAspectSouth;
float z = halfDepth - depth * fac;
for (int j = 0; j < lonsp1; ++j)
{
int jMod = j % longitudes;
Vector2 tc = thetaCartesian[jMod];
Vector2 rtc = rhoThetaCartesian[jMod];
float sTexture = sTextureCache[j];
vs[idxCylLat] = new Vector3(rtc.x, z, -rtc.y);
vts[idxCylLat] = new Vector2(sTexture, tTexture);
vns[idxCylLat] = new Vector3(tc.x, 0.0f, -tc.y);
++idxCylLat;
}
}
}
// Triangle indices.
// Stride is 3 for polar triangles;
// stride is 6 for two triangles forming a quad.
int lons3 = longitudes * 3;
int lons6 = longitudes * 6;
int hemiLons = halfLatsn1 * lons6;
int triOffsetNorthHemi = lons3;
int triOffsetCylinder = triOffsetNorthHemi + hemiLons;
int triOffsetSouthHemi = triOffsetCylinder + ringsp1 * lons6;
int triOffsetSouthCap = triOffsetSouthHemi + hemiLons;
int fsLen = triOffsetSouthCap + lons3;
int[] tris = new int[fsLen];
// Polar caps.
for (int i = 0, k = 0, m = triOffsetSouthCap; i < longitudes; ++i, k += 3, m += 3)
{
// North.
tris[k] = i;
tris[k + 1] = vertOffsetNorthHemi + i;
tris[k + 2] = vertOffsetNorthHemi + i + 1;
// South.
tris[m] = vertOffsetSouthCap + i;
tris[m + 1] = vertOffsetSouthPolar + i + 1;
tris[m + 2] = vertOffsetSouthPolar + i;
}
// Hemispheres.
for (int i = 0, k = triOffsetNorthHemi, m = triOffsetSouthHemi; i < halfLatsn1; ++i)
{
int iLonsp1 = i * lonsp1;
int vertCurrLatNorth = vertOffsetNorthHemi + iLonsp1;
int vertNextLatNorth = vertCurrLatNorth + lonsp1;
int vertCurrLatSouth = vertOffsetSouthEquator + iLonsp1;
int vertNextLatSouth = vertCurrLatSouth + lonsp1;
for (int j = 0; j < longitudes; ++j, k += 6, m += 6)
{
// North.
int north00 = vertCurrLatNorth + j;
int north01 = vertNextLatNorth + j;
int north11 = vertNextLatNorth + j + 1;
int north10 = vertCurrLatNorth + j + 1;
tris[k] = north00;
tris[k + 1] = north11;
tris[k + 2] = north10;
tris[k + 3] = north00;
tris[k + 4] = north01;
tris[k + 5] = north11;
// South.
int south00 = vertCurrLatSouth + j;
int south01 = vertNextLatSouth + j;
int south11 = vertNextLatSouth + j + 1;
int south10 = vertCurrLatSouth + j + 1;
tris[m] = south00;
tris[m + 1] = south11;
tris[m + 2] = south10;
tris[m + 3] = south00;
tris[m + 4] = south01;
tris[m + 5] = south11;
}
}
// Cylinder.
for (int i = 0, k = triOffsetCylinder; i < ringsp1; ++i)
{
int vertCurrLat = vertOffsetNorthEquator + i * lonsp1;
int vertNextLat = vertCurrLat + lonsp1;
for (int j = 0; j < longitudes; ++j, k += 6)
{
int cy00 = vertCurrLat + j;
int cy01 = vertNextLat + j;
int cy11 = vertNextLat + j + 1;
int cy10 = vertCurrLat + j + 1;
tris[k] = cy00;
tris[k + 1] = cy11;
tris[k + 2] = cy10;
tris[k + 3] = cy00;
tris[k + 4] = cy01;
tris[k + 5] = cy11;
}
}
var mesh = new Mesh();
mesh.vertices = vs;
mesh.uv = vts;
mesh.normals = vns;
// Triangles must be assigned last.
mesh.triangles = tris;
mesh.RecalculateTangents();
mesh.Optimize();
return mesh;
}
}
}