Dave Noonan wrote:
I do have data for the wheel sizes of many of the cars in Horizon but not the DLC ones.
I use that data to scale the wheels but the big problem is with the tires.
3DSimED is using the same tire on every car because I cannot track down which tire model each car is using.
The tires are not the correct size because although I have the aspect ratio for the tires I don't know how to quickly rescale the vertices of the tire model to get the profile right. I am sure that there must be something in the tire models to enable FH3 to create different profile tires I cannot find that data so I'm a bit stuck.
Cheers!
Dave N.
br555 wrote:
Thanks Dave, the Fiat 8V Supersonic now exports to FBX without issues; Wheels and brakes look better too
You've already lavished plenty of attention on the FH3 models (thank-you), but if you are interested I have some C# code for resizing the generic FH3 tire meshes to a given tire spec e.g. 255/45-17. It's intended for the Unity environment but it should be easy to understand/port. The open question is how/where it would get the tire size info for a given vehicle as I don't know if that's accessible in FH3 metadata
I can't help with the automatic tire model selection - maybe by year? I can help with re-scaling tire model vertices. I'm not sure how FH3 does it, but I'm using a brute-force approach that seems to work ok. I've also included my code for sizing the rims, as I size those according to the tire specs so everything mates properly. So far this code has worked on all FH3 tire and wheel models (the tire code even works on some non-Forza tire meshes).
I'm convinced there is a bug in Unity where imported models (e.g. fbx) come into the environment a bit larger than the import scale specifies, so you may need to adjust the global scale a bit to get the final product dimensionally accurate.
Note that I'm manually resizing the rim meshes (rims only, not tires) to 80% prior to calling the resize rim function. This is so that the spoke/hub/outer rim portion of the wheel has (approximately) the correct thickness, then the function takes over and adjusts only the inner barrel portion of the rim to accommodate different rim section widths without further distorting the hub/spoke area.
The code also laterally centers the wheel origin, which you may or may not want.
Code:
public enum WheelSide { left, right };
public const float RIM_LIP_WIDTH = 0.01905f; //this constant roughly bridges the gap between a standard tire's rim bead diameter (e.g. 17" for a 255/45-17 tire) and the inner radius of an equivalent FH3 tire mesh
protected static void FormatFH3Wheel(GameObject g, float tireSectionWidth, float wheelRimDiameter) {
//IMPORTANT: Please note that all wheel meshes have been manually scaled to 80% size prior to this processing so we are starting with a wheel that should be 0.8 meters wide (although the code doesn't rely on that)
//this two-stage scaling is required because the wheel hub/spokes/outer rim must be scaled down to get the desired hub/spoke depth; The global 80% pre-scaling accomplishes this
//the inner rim portion is scaled much more drastically to accomodate varying rim section widths, which is the goal of this function
//example usage: FormatFH3Wheel (g, 0.255f, 17f); //will create a 17" wheel that is sized to fit a 255mm section width tire
//note that just like real rims the actual extents of the wheel mesh (i.e. lip included) will be taller than 17" (more like 18+")
//unlike real life rims however, our rim section widths will always be (nearly) identical to tire section width because I haven't figured out how to deform the tire sidwall profile to accommodate varying rim widths for a given tire section width
if (g != null) {
Mesh mesh = g.GetComponentInChildren<MeshFilter>().sharedMesh;
int[][] triangles = new int[mesh.subMeshCount][];
for (int i = 0; i < mesh.subMeshCount; i++) {
triangles[i] = new int[mesh.GetTriangles(i).Length];
triangles[i] = mesh.GetTriangles(i);
}
Vector3[] vertices = mesh.vertices;
Vector2[] uv = mesh.uv;
float targetRimRadius = wheelRimDiameter * Convert.inchesToMeters * 0.5f + RIM_LIP_WIDTH;
float sourceRimRadius = GetOuterRadius (vertices);
float targetRadiusScaleFactor = targetRimRadius / sourceRimRadius;
//shorten the width of all verticies on the inner 10% of the rim which will narrow the rim without distorting the wheel face on the outer side
float sourceWheelWidth = GetWheelWidth(vertices);
float widthScaleFactor = (tireSectionWidth - 0.004f) / sourceWheelWidth; //0.004f is to account for the outer wheel lip thickness (which we are not scaling but still makes up a small part of the total wheel width)
float wheelCenteringOffset = tireSectionWidth * 0.5f * ((GetWheelSide (vertices) == WheelSide.left) ? -1f : 1f); //for moving the lateral pivot point of the wheel from the outer edge to the center
for (int i = 0; i < vertices.Length; i++) {
if (Mathf.Abs(vertices[i].x) > sourceWheelWidth * 0.9f)
vertices [i] = new Vector3 (vertices [i].x * widthScaleFactor, vertices [i].y, vertices [i].z);
vertices [i] = new Vector3 (vertices [i].x + wheelCenteringOffset, vertices [i].y * targetRadiusScaleFactor, vertices [i].z * targetRadiusScaleFactor);
}
//done, update shared mesh
for (int i = 0; i < mesh.subMeshCount; i++)
mesh.SetTriangles(triangles[i], i);
mesh.vertices = vertices;
mesh.uv = uv;
mesh.RecalculateBounds ();
}
}
protected static float GetWheelWidth(Vector3[] vertices) {
float outerX = float.MinValue;
float innerX = float.MaxValue;
//width = largest x - smallest x
for (int i = 0; i < vertices.Length; i++) {
if (vertices[i].x > outerX)
outerX = vertices[i].x;
if (vertices[i].x < innerX)
innerX = vertices[i].x;
}
return Mathf.Abs (outerX - innerX);
}
protected static WheelSide GetWheelSide(Vector3[] vertices) {
return (float)vertices.Average (v => v.x) > 0f ? WheelSide.left : WheelSide.right;
}
protected void ResizeForzaHorizon3Tire(GameObject g) {
float sectionWidth = 0f; //mm
float aspectRatio = 0f;
float wheelRimDiameter = 0f; //in
Mesh mesh = null;
int[][] triangles = null;
Vector3[] vertices = null;
Vector2[] uv = null;
float sourceInnerRadius = 0f;
float targetInnerRadius = 0f;
float sourceOuterRadius = 0f;
float targetOuterRadius = 0f;
float sourceSectionHeight = 0f;
float targetSectionHeight = 0f;
float targetInnerRadiusScaleFactor = 0f; //how much to scale the innermost sidewall to achive the desired "rim" diameter
float innerRadiusScaleFactor = 0f;
float sourceTireWidth = 0f;
float tireWidthScaleFactor = 0f;
float sidewallProfileWidthScale = 0f;
//note that the FH3 tire meshes understandably do not model a proper rim bead as that would be a waste of polygons
//their inner radius starts near the outer edge of the rim flange/lip such that only a tiny bit of the tire sidewall mesh is obscured by the rim mesh
//const float RIM_LIP_WIDTH = 0.01905f; //this constant roughly bridges the gap between a standard tire's rim bead diameter (e.g. 17" for a 255/45-17 tire) and the inner radius of an equivalent FH3 "beadless" tire mesh
float wheelCenteringOffset = 0f;
string[] tireParams = g.name.Split ('_');
if (tireParams.Length != 4)
return;
if (!float.TryParse (tireParams [1], out sectionWidth))
return;
if (!float.TryParse (tireParams [2], out aspectRatio))
return;
if (!float.TryParse (tireParams [3], out wheelRimDiameter))
return;
targetSectionHeight = sectionWidth * 0.001f * aspectRatio * 0.01f - RIM_LIP_WIDTH;
targetOuterRadius = wheelRimDiameter * Convert.inchesToMeters * 0.5f + RIM_LIP_WIDTH + targetSectionHeight;
targetInnerRadius = wheelRimDiameter * Convert.inchesToMeters * 0.5f + RIM_LIP_WIDTH;
mesh = g.GetComponent<MeshFilter>().sharedMesh;
triangles = new int[mesh.subMeshCount][];
for (int i = 0; i < mesh.subMeshCount; i++) {
triangles[i] = new int[mesh.GetTriangles(i).Length];
triangles[i] = mesh.GetTriangles(i);
}
vertices = mesh.vertices;
uv = mesh.uv;
sourceInnerRadius = GetInnerTireRadius (vertices);
sourceOuterRadius = GetOuterRadius (vertices);
sourceTireWidth = GetWheelWidth (vertices);
wheelCenteringOffset = sectionWidth * 0.001f * 0.5f * ((GetWheelSide (vertices) == WheelSide.left) ? -1f : 1f);
tireWidthScaleFactor = sectionWidth * 0.001f / sourceTireWidth;
sourceSectionHeight = sourceOuterRadius - sourceInnerRadius;
innerRadiusScaleFactor = targetInnerRadius / sourceInnerRadius;
sidewallProfileWidthScale = sourceSectionHeight / targetSectionHeight;
for (int i = 0; i < vertices.Length; i++) {
float vertAngle = Mathf.Atan2(vertices [i].y, vertices [i].z);
float vertLength = Mathf.Sqrt(Mathf.Pow(Mathf.Abs(vertices [i].z), 2f) + Mathf.Pow(Mathf.Abs(vertices [i].y), 2f));
float vertSidewallScale = (vertLength - sourceInnerRadius) / sourceSectionHeight;
float newVertLength = targetInnerRadius + (targetSectionHeight * vertSidewallScale);
//as we squish sidewall vertically we need to flatten it horizontally in the same proportion in order to keep the same profile
//(1f + (1f-sidewallProfileWidthScale)) term is to restore the desired section width after adjusting the profile (we need to apply this only to the sidewall verts and not the tread)
vertices [i] = new Vector3 (vertices [i].x * tireWidthScaleFactor + wheelCenteringOffset, Mathf.Sin (vertAngle) * newVertLength, Mathf.Cos (vertAngle) * newVertLength);
}
//done, now update mesh
for (int i = 0; i < mesh.subMeshCount; i++) {
mesh.SetTriangles(triangles[i], i);
}
mesh.vertices = vertices;
mesh.uv = uv;
mesh.RecalculateNormals ();
mesh.RecalculateBounds ();
}
protected static float GetInnerTireRadius(Vector3[] vertices) {
float innerRadius = float.MaxValue;
for (int i = 0; i < vertices.Length; i++) {
float vertLength = Mathf.Sqrt(Mathf.Pow(Mathf.Abs(vertices [i].z) - 0, 2) + Mathf.Pow(Mathf.Abs(vertices [i].y) - 0, 2));
if (vertLength < innerRadius)
innerRadius = vertLength;
}
return innerRadius;
}
protected static float GetOuterRadius(Vector3[] vertices) {
float outerRadius = 0f;
for (int i = 0; i < vertices.Length; i++) {
float vertLength = Mathf.Sqrt(Mathf.Pow(Mathf.Abs(vertices [i].z) - 0, 2) + Mathf.Pow(Mathf.Abs(vertices [i].y) - 0, 2));
if (vertLength > outerRadius)
outerRadius = vertLength;
}
return outerRadius;
}