mirror of
https://github.com/Outer-Wilds-New-Horizons/new-horizons.git
synced 2025-12-11 20:15:44 +01:00
508 lines
18 KiB
C#
508 lines
18 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.Reflection;
|
||
|
||
public static class NomaiTextArcBuilder {
|
||
public static int i = 0;
|
||
|
||
public static SpiralProfile spiralProfile;
|
||
public static bool removeBakedInRotationAndPosition = true;
|
||
|
||
public static void PlaceAdult()
|
||
{
|
||
BuildSpiralGameObject(adultSpiralProfile, "Text Arc Prefab " + (i++));
|
||
}
|
||
public static void PlaceChild()
|
||
{
|
||
BuildSpiralGameObject(childSpiralProfile, "Text Arc Prefab " + (i++));
|
||
}
|
||
|
||
public static GameObject BuildSpiralGameObject(SpiralProfile profile, string goName="New Nomai Spiral")
|
||
{
|
||
var g = new GameObject(goName);
|
||
g.transform.localPosition = Vector3.zero;
|
||
g.transform.localEulerAngles = Vector3.zero;
|
||
|
||
var m = new SpiralMesh(profile);
|
||
m.Randomize();
|
||
m.updateMesh();
|
||
|
||
g.AddComponent<MeshFilter>().sharedMesh = m.mesh;
|
||
g.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Sprites/Default"));
|
||
g.GetComponent<MeshRenderer>().sharedMaterial.color = Color.magenta;
|
||
|
||
var owNomaiTextLine = g.AddComponent<NomaiTextLine>();
|
||
|
||
//
|
||
// rotate mesh to face up
|
||
//
|
||
|
||
var norm = m.skeleton[1] - m.skeleton[0];
|
||
float r = Mathf.Atan2(-norm.y, norm.x) * Mathf.Rad2Deg;
|
||
if (m.mirror) r += 180;
|
||
var ang = m.mirror ? 90-r : -90-r;
|
||
|
||
// using m.sharedMesh causes old meshes to disappear for some reason, idk why
|
||
var mesh = g.GetComponent<MeshFilter>().mesh;
|
||
if (removeBakedInRotationAndPosition)
|
||
{
|
||
var meshCopy = mesh;
|
||
var newVerts = meshCopy.vertices.Select(v => Quaternion.Euler(-90, 0, 0) * Quaternion.Euler(0, ang, 0) * v).ToArray();
|
||
meshCopy.vertices = newVerts;
|
||
meshCopy.RecalculateBounds();
|
||
}
|
||
|
||
AssetDatabase.CreateAsset(mesh, "Assets/Spirals/"+(profile.profileName)+"spiral" + (NomaiTextArcBuilder.i) + ".asset");
|
||
g.GetComponent<MeshFilter>().sharedMesh = AssetDatabase.LoadAssetAtPath("Assets/Spirals/"+(profile.profileName)+"spiral" + (NomaiTextArcBuilder.i) + ".asset", typeof(Mesh)) as Mesh;
|
||
NomaiTextArcBuilder.i++;
|
||
|
||
//
|
||
// set up NomaiTextArc stuff
|
||
//
|
||
|
||
var _points = m.skeleton
|
||
.Select((compiled) =>
|
||
Quaternion.Euler(-90, 0, 0) * Quaternion.Euler(0, ang, 0) * (new Vector3(compiled.x, 0, compiled.y)) // decompile them, rotate them by ang, and then rotate them to be vertical, like the base game spirals are
|
||
)
|
||
.ToList();
|
||
|
||
var _lengths = _points.Take(_points.Count()-1).Select((point, i) => Vector3.Distance(point, _points[i+1])).ToArray();
|
||
var _totalLength = _lengths.Aggregate(0f, (acc, length) => acc + length);
|
||
var _state = NomaiTextLine.VisualState.UNREAD;
|
||
var _textLineLocation = NomaiText.Location.UNSPECIFIED;
|
||
var _center = _points.Aggregate(Vector3.zero, (acc, point) => acc + point) / (float)_points.Count();
|
||
var _radius = _points.Aggregate(0f, (acc, point) => Mathf.Max(Vector3.Distance(_center, point), acc));
|
||
var _active = true;
|
||
|
||
(typeof (NomaiTextLine)).InvokeMember("_points", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _points.ToArray() });
|
||
(typeof (NomaiTextLine)).InvokeMember("_lengths", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _lengths });
|
||
(typeof (NomaiTextLine)).InvokeMember("_totalLength", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _totalLength });
|
||
(typeof (NomaiTextLine)).InvokeMember("_state", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _state });
|
||
(typeof (NomaiTextLine)).InvokeMember("_textLineLocation", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _textLineLocation });
|
||
(typeof (NomaiTextLine)).InvokeMember("_center", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _center });
|
||
(typeof (NomaiTextLine)).InvokeMember("_radius", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _radius });
|
||
(typeof (NomaiTextLine)).InvokeMember("_active", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic, null, owNomaiTextLine, new object[] { _active });
|
||
|
||
return g;
|
||
}
|
||
|
||
//
|
||
//
|
||
// Handle the connection between game objects and spiral meshes
|
||
//
|
||
//
|
||
|
||
public struct SpiralProfile {
|
||
// all of the Vector2 params here refer to a range of valid values
|
||
public string profileName;
|
||
public bool canMirror;
|
||
public Vector2 a;
|
||
public Vector2 b;
|
||
public Vector2 endS;
|
||
public Vector2 skeletonScale;
|
||
public int numSkeletonPoints;
|
||
public float uvScale;
|
||
public float innerWidth; // width at the tip
|
||
public float outerWidth; // width at the base
|
||
public Material material;
|
||
}
|
||
|
||
public static SpiralProfile adultSpiralProfile = new SpiralProfile() {
|
||
profileName="Adult",
|
||
canMirror = false, // we don't want to mirror the actual mesh itself anymore, we'll just mirror the game object using localScale.x
|
||
a = new Vector2(0.5f, 0.5f),
|
||
b = new Vector2(0.3f, 0.6f),
|
||
endS = new Vector2(0, 50f),
|
||
skeletonScale = new Vector2(0.01f, 0.01f),
|
||
numSkeletonPoints = 51,
|
||
|
||
innerWidth = 0.001f,
|
||
outerWidth = 0.05f,
|
||
uvScale = 4.9f,
|
||
};
|
||
|
||
public static SpiralProfile childSpiralProfile = new SpiralProfile() {
|
||
profileName="Child",
|
||
canMirror = false, // we don't want to mirror the actual mesh itself anymore, we'll just mirror the game object using localScale.x
|
||
a = new Vector2(0.9f, 0.9f),
|
||
b = new Vector2(0.305f, 0.4f),
|
||
endS = new Vector2(16f, 60f),
|
||
skeletonScale = new Vector2(0.002f, 0.005f),
|
||
numSkeletonPoints = 51,
|
||
|
||
innerWidth = 0.001f/10f,
|
||
outerWidth = 2f*0.05f,
|
||
uvScale = 4.9f/3.5f,
|
||
};
|
||
|
||
//
|
||
//
|
||
// Construct spiral meshes from the mathematical spirals generated below
|
||
//
|
||
//
|
||
|
||
public class SpiralMesh: MathematicalSpiral {
|
||
public List<Vector3> skeleton;
|
||
public List<Vector2> skeletonOutsidePoints;
|
||
|
||
public int numSkeletonPoints = 51; // seems to be Mobius' default
|
||
|
||
public float innerWidth = 0.001f; // width at the tip
|
||
public float outerWidth = 0.05f; //0.107f; // width at the base
|
||
public float uvScale = 4.9f; //2.9f;
|
||
private float baseUVScale = 1f / 300f;
|
||
public float uvOffset = 0;
|
||
|
||
public Mesh mesh;
|
||
|
||
public SpiralMesh(SpiralProfile profile): base(profile) {
|
||
this.numSkeletonPoints = profile.numSkeletonPoints;
|
||
this.innerWidth = profile.innerWidth;
|
||
this.outerWidth = profile.outerWidth;
|
||
this.uvScale = profile.uvScale;
|
||
|
||
this.uvOffset = UnityEngine.Random.value;
|
||
}
|
||
|
||
public override void Randomize() {
|
||
base.Randomize();
|
||
uvOffset = UnityEngine.Random.value; // this way even two spirals that are exactly the same shape will look different (this changes the starting point of the handwriting texture)
|
||
}
|
||
|
||
internal void updateMesh() {
|
||
skeleton = this.getSkeleton(numSkeletonPoints);
|
||
skeletonOutsidePoints = this.getSkeletonOutsidePoints(numSkeletonPoints);
|
||
|
||
List<Vector3> vertsSide1 = skeleton.Select((skeletonPoint, index) => {
|
||
Vector3 normal = new Vector3(cos(skeletonPoint.z), 0, sin(skeletonPoint.z));
|
||
float width = lerp(((float) index) / ((float) skeleton.Count()), outerWidth, innerWidth);
|
||
|
||
return new Vector3(skeletonPoint.x, 0, skeletonPoint.y) + width * normal;
|
||
}).ToList();
|
||
|
||
List<Vector3> vertsSide2 = skeleton.Select((skeletonPoint, index) => {
|
||
Vector3 normal = new Vector3(cos(skeletonPoint.z), 0, sin(skeletonPoint.z));
|
||
float width = lerp(((float) index) / ((float) skeleton.Count()), outerWidth, innerWidth);
|
||
|
||
return new Vector3(skeletonPoint.x, 0, skeletonPoint.y) - width * normal;
|
||
}).ToList();
|
||
|
||
Vector3[] newVerts = vertsSide1.Zip(vertsSide2, (f, s) => new [] {
|
||
f,
|
||
s
|
||
}).SelectMany(f =>f).ToArray(); // interleave vertsSide1 and vertsSide2
|
||
|
||
List<int> triangles = new List<int>();
|
||
for (int i = 0; i<newVerts.Length - 2; i += 2) {
|
||
/*
|
||
| ⟍ |
|
||
| ⟍|
|
||
2 *-----* 3
|
||
|⟍ |
|
||
| ⟍ |
|
||
| ⟍|
|
||
0 *-----* 1
|
||
|⟍ |
|
||
*/
|
||
triangles.Add(i + 2);
|
||
triangles.Add(i + 1);
|
||
triangles.Add(i);
|
||
|
||
triangles.Add(i + 2);
|
||
triangles.Add(i + 3);
|
||
triangles.Add(i + 1);
|
||
}
|
||
|
||
var startT = tFromArcLen(startS);
|
||
var endT = tFromArcLen(endS);
|
||
|
||
var rangeT = endT - startT;
|
||
var rangeS = endS - startS;
|
||
|
||
Vector2[] uvs = new Vector2[newVerts.Length];
|
||
Vector2[] uv2s = new Vector2[newVerts.Length];
|
||
for (int i = 0; i<skeleton.Count(); i++) {
|
||
float fraction = 1 - ((float) i) / ((float) skeleton.Count()); // casting is so uuuuuuuugly
|
||
|
||
// note: cutting the sprial into numPoints equal slices of arclen would
|
||
// provide evenly spaced skeleton points
|
||
// on the other hand, cutting the spiral into numPoints equal slices of t
|
||
// will cluster points in areas of higher detail. this is the way Mobius does it, so it is the way we also will do it
|
||
float inputT = startT + rangeT * fraction;
|
||
float inputS = tToArcLen(inputT);
|
||
float sFraction = (inputS - startS) / rangeS;
|
||
float absoluteS = (inputS - startS);
|
||
|
||
float u = absoluteS * uvScale * baseUVScale + uvOffset;
|
||
uvs[i * 2] = new Vector2(u, 0);
|
||
uvs[i * 2 + 1] = new Vector2(u, 1);
|
||
|
||
uv2s[i * 2] = new Vector2(1 - sFraction, 0);
|
||
uv2s[i * 2 + 1] = new Vector2(1 - sFraction, 1);
|
||
}
|
||
|
||
Vector3[] normals = new Vector3[newVerts.Length];
|
||
for (int i = 0; i<newVerts.Length; i++) normals[i] = new Vector3(0, 0, 1);
|
||
|
||
if (mesh == null){
|
||
mesh = new Mesh();
|
||
}
|
||
mesh.vertices = newVerts.ToArray();
|
||
mesh.triangles = triangles.ToArray().Reverse().ToArray(); // triangles need to be reversed so the spirals face the right way (I generated them backwards above, on accident)
|
||
mesh.uv = uvs;
|
||
mesh.uv2 = uv2s;
|
||
mesh.normals = normals;
|
||
mesh.RecalculateBounds();
|
||
}
|
||
}
|
||
|
||
//
|
||
//
|
||
// Construct the mathematical spirals that Nomai arcs are built from
|
||
//
|
||
//
|
||
|
||
public class MathematicalSpiral {
|
||
public bool mirror;
|
||
public float a;
|
||
public float b; // 0.3-0.6
|
||
public float startSOnParent;
|
||
public float scale;
|
||
public List<MathematicalSpiral> children;
|
||
|
||
public float x;
|
||
public float y;
|
||
public float ang;
|
||
|
||
public float startS = 42.87957f; // go all the way down to 0, all the way up to 50
|
||
public float endS = 342.8796f;
|
||
|
||
SpiralProfile profile;
|
||
|
||
public MathematicalSpiral(SpiralProfile profile) {
|
||
this.profile = profile;
|
||
|
||
this.Randomize();
|
||
}
|
||
|
||
public MathematicalSpiral(float startSOnParent = 0, bool mirror = false, float len = 300, float a = 0.5f, float b = 0.43f, float scale = 0.01f) {
|
||
this.mirror = mirror;
|
||
this.a = a;
|
||
this.b = b;
|
||
this.startSOnParent = startSOnParent;
|
||
this.scale = scale;
|
||
|
||
this.children = new List<MathematicalSpiral>();
|
||
|
||
this.x = 0;
|
||
this.y = 0;
|
||
this.ang = 0;
|
||
}
|
||
|
||
public virtual void Randomize() {
|
||
this.a = UnityEngine.Random.Range(profile.a.x, profile.a.y); //0.5f;
|
||
this.b = UnityEngine.Random.Range(profile.b.x, profile.b.y);
|
||
this.startS = UnityEngine.Random.Range(profile.endS.x, profile.endS.y);
|
||
this.scale = UnityEngine.Random.Range(profile.skeletonScale.x, profile.skeletonScale.y);
|
||
if (profile.canMirror) this.mirror = UnityEngine.Random.value<0.5f;
|
||
}
|
||
|
||
internal virtual void updateChild(MathematicalSpiral child) {
|
||
Vector3 pointAndNormal = getDrawnSpiralPointAndNormal(child.startSOnParent);
|
||
var cx = pointAndNormal.x;
|
||
var cy = pointAndNormal.y;
|
||
var cang = pointAndNormal.z;
|
||
child.x = cx;
|
||
child.y = cy;
|
||
child.ang = cang + (child.mirror ? Mathf.PI : 0);
|
||
}
|
||
|
||
public virtual void addChild(MathematicalSpiral child) {
|
||
updateChild(child);
|
||
this.children.Add(child);
|
||
}
|
||
|
||
public virtual void updateChildren() {
|
||
this.children.ForEach(child => {
|
||
updateChild(child);
|
||
child.updateChildren();
|
||
});
|
||
}
|
||
|
||
// note: each Vector3 in this list is of form <x, y, angle in radians of the normal at this point>
|
||
public List<Vector3> getSkeleton(int numPoints) {
|
||
var skeleton =
|
||
WalkAlongSpiral(numPoints)
|
||
.Select(input => {
|
||
float inputS = input.y;
|
||
var skeletonPoint = getDrawnSpiralPointAndNormal(inputS);
|
||
return skeletonPoint;
|
||
})
|
||
.Reverse()
|
||
.ToList();
|
||
|
||
return skeleton;
|
||
}
|
||
|
||
public List<Vector2> getSkeletonOutsidePoints(int numPoints) {
|
||
var outsidePoints =
|
||
WalkAlongSpiral(numPoints)
|
||
.Select(input => {
|
||
float inputT = input.x;
|
||
float inputS = input.y;
|
||
|
||
var skeletonPoint = getDrawnSpiralPointAndNormal(inputS);
|
||
|
||
var deriv = spiralDerivative(inputT);
|
||
var outsidePoint = new Vector2(skeletonPoint.x, skeletonPoint.y) - (new Vector2(-deriv.y, deriv.x)).normalized * 0.1f;
|
||
return outsidePoint;
|
||
})
|
||
.Reverse()
|
||
.ToList();
|
||
|
||
return outsidePoints;
|
||
}
|
||
|
||
// generate a list of <inputT, inputS> evenly distributed over the whole spiral. `numPoints` number of <inputT, inputS> pairs are generated
|
||
public IEnumerable<Vector2> WalkAlongSpiral(int numPoints) {
|
||
var endT = tFromArcLen(endS);
|
||
var startT = tFromArcLen(startS);
|
||
var rangeT = endT - startT;
|
||
|
||
for (int i = 0; i<numPoints; i++) {
|
||
float fraction = ((float) i) / ((float) numPoints - 1f); // casting is so uuuuuuuugly
|
||
|
||
// note: cutting the sprial into numPoints equal slices of arclen would
|
||
// provide evenly spaced skeleton points
|
||
// on the other hand, cutting the spiral into numPoints equal slices of t
|
||
// will cluster points in areas of higher detail. this is the way Mobius does it, so it is the way we also will do it
|
||
float inputT = startT + rangeT * fraction;
|
||
float inputS = tToArcLen(inputT);
|
||
|
||
yield return new Vector2(inputT, inputS);
|
||
}
|
||
}
|
||
|
||
// all of this math is based off of this:
|
||
// https://www.desmos.com/calculator/9gdfgyuzf6
|
||
//
|
||
// note: t refers to theta, and s refers to arc length
|
||
//
|
||
|
||
// get the (x, y) coordinates and the normal angle at the given location (measured in arcLen) of a spiral with the given parameters
|
||
// note: arcLen is inverted so that 0 refers to what we consider the start of the Nomai spiral
|
||
public Vector3 getDrawnSpiralPointAndNormal(float arcLen) {
|
||
float offsetX = this.x;
|
||
float offsetY = this.y;
|
||
float offsetAngle = this.ang;
|
||
var startS = this.endS; // I know this is funky, but just go with it for now.
|
||
|
||
var startT = tFromArcLen(startS); // this is the `t` value for the root of the spiral (the end of the non-curled side)
|
||
|
||
var startPoint = spiralPoint(startT); // and this is the (x,y) location of the non-curled side, relative to the rest of the spiral. we'll offset everything so this is at (0,0) later
|
||
var startX = startPoint.x;
|
||
var startY = startPoint.y;
|
||
|
||
var t = tFromArcLen(arcLen);
|
||
var point = spiralPoint(t); // the absolute (x,y) location that corresponds to `arcLen`, before accounting for things like putting the start point at (0,0), or dealing with offsetX/offsetY/offsetAngle
|
||
var x = point.x;
|
||
var y = point.y;
|
||
var ang = normalAngle(t);
|
||
|
||
if (mirror) {
|
||
x = x + 2 * (startX - x);
|
||
ang = -ang + Mathf.PI;
|
||
}
|
||
|
||
// translate so that startPoint is at (0,0)
|
||
// (also scale the spiral)
|
||
var retX = scale * (x - startX);
|
||
var retY = scale * (y - startY);
|
||
|
||
// rotate offsetAngle rads
|
||
var retX2 = retX * cos(offsetAngle) -
|
||
retY * sin(offsetAngle);
|
||
var retY2 = retX * sin(offsetAngle) +
|
||
retY * cos(offsetAngle);
|
||
|
||
retX = retX2;
|
||
retY = retY2;
|
||
|
||
// translate for offsetX, offsetY
|
||
retX += offsetX;
|
||
retY += offsetY;
|
||
|
||
return new Vector3(retX, retY, ang + offsetAngle + Mathf.PI / 2f);
|
||
}
|
||
|
||
// the base formula for the spiral
|
||
public Vector2 spiralPoint(float t) {
|
||
var r = a * exp(b * t);
|
||
var retval = new Vector2(r * cos(t), r * sin(t));
|
||
return retval;
|
||
}
|
||
|
||
// the spiral's got two functions: x(t) and y(t)
|
||
// so it's got two derrivatives (with respect to t) x'(t) and y'(t)
|
||
public Vector2 spiralDerivative(float t) { // derrivative with respect to t
|
||
var r = a * exp(b * t);
|
||
return new Vector2(
|
||
-r * (sin(t) - b * cos(t)),
|
||
r * (b * sin(t) + cos(t))
|
||
);
|
||
}
|
||
|
||
// returns the length of the spiral between t0 and t1
|
||
public float spiralArcLength(float t0, float t1) {
|
||
return (a / b) * sqrt(b * b + 1) * (exp(b * t1) - exp(b * t0));
|
||
}
|
||
|
||
// converts from a value of t to the equivalent value of s (the value of s that corresponds to the same point on the spiral as t)
|
||
public float tToArcLen(float t) {
|
||
return spiralArcLength(0, t);
|
||
}
|
||
|
||
// reverse of above
|
||
public float tFromArcLen(float s) {
|
||
return ln(
|
||
1 + s / (
|
||
(a / b) *
|
||
sqrt(b * b + 1)
|
||
)
|
||
) / b;
|
||
}
|
||
|
||
// returns the angle of the spiral's normal at a given point
|
||
public float normalAngle(float t) {
|
||
var d = spiralDerivative(t);
|
||
var n = new Vector2(d.y, -d.x);
|
||
var angle = Mathf.Atan2(n.y, n.x);
|
||
|
||
return angle - Mathf.PI / 2;
|
||
}
|
||
}
|
||
|
||
// convenience, so the math above is more readable
|
||
private static float lerp(float a, float b, float t) {
|
||
return a * t + b * (1 - t);
|
||
}
|
||
|
||
private static float cos(float t) {
|
||
return Mathf.Cos(t);
|
||
}
|
||
private static float sin(float t) {
|
||
return Mathf.Sin(t);
|
||
}
|
||
private static float exp(float t) {
|
||
return Mathf.Exp(t);
|
||
}
|
||
private static float sqrt(float t) {
|
||
return Mathf.Sqrt(t);
|
||
}
|
||
private static float ln(float t) {
|
||
return Mathf.Log(t);
|
||
}
|
||
} |