How to rotate a 3D object based on stick Input and the normal of the ground's surface? (Unity3D)

I am making a game where I want to view an object from any angle and use an analog stick to rotate it according to this input and the surface the object is currently resting on. Here's a visualization of what I am trying to accomplish to outline my question:

(this is from a camera perspective, user's view. Also that's "transform.forward", woops)

At the moment I go about some of the initial steps like this with a short cylinder (think a hockey puck):

1. Take the average normal below the puck using 8 raycasts around the rim towards the ground
2. Use this average normal to have the puck mimic the surface's average tilt
3. Use the camera's rotation to as the reference for determining where the stick is pointing from the player's POV
4. In terms of the diagram, this is the step where I'd hope to calculate the orange arrow (the output direction of the object's forward direction), but so far I cannot figure that part out.

To specify, I want to apply force based on the transform's forward direction relative to the ground so simply applying force on an XY plane is not what I am trying to accomplish. Below is my best attempt so far. It fails in that it doesn't turn on the ground according to its movement on the plane.

// All the objects I am using to capture/store the data
GameObject input;
GameObject camera_Axis;

GameObject tank_Manager;
bool isGrounded;

GameObject base_Model; // The "puck's" model
GameObject base_Collider; // The "puck's" collider
GameObject ground_Checker; // Object which casts the 8 rays directly down from puck rim
float groundMatchSpeed; // Speed the objects mimics the ground surface
float turnSpeed; // Speed at which the object would turn left and right relative to the surface

float lastAngleLSFromOrigin;
Vector3 lastPosition;

PlayerControls moveControl;
Vector2 move;

void Awake()
{
moveControl = new PlayerControls();
}
void OnEnable()
{
moveControl.Gameplay.Enable();
}
void OnDisable()
{
moveControl.Gameplay.Disable();
}

void Update()
{
Quaternion rotation = base_Collider.transform.rotation;

float LSx = move[0];
float LSy = -move[1];
Vector2 LS = new Vector2(LSx, LSy); // Stick inputs

float angleLSFromOrigin = Vector2.SignedAngle(new Vector2(0f, -1f), LS);
if ((LSx == 0) & (LSy == 0))
{
angleLSFromOrigin = lastAngleLSFromOrigin; // If there is any input, update the angle
}

Quaternion relative = camera_Axis.transform.rotation;

if (LS.magnitude >= 1)
{
lastAngleLSFromOrigin = angleLSFromOrigin; // If stick is beyond threshold, update angle
}

if (rotation == camera_Axis.transform.rotation)
{
relative = Quaternion.Inverse(rotation) * yRotation(camera_Axis.transform.rotation);
// I am going to be honest, I don't remember why this works and how, but it is essential. Fixes a problem where the object and camera align perfectly
}

// Determine ground tilt from hover test points
Vector3[] hover_Test_Normals = ground_Checker.GetComponent<check_Grounded>().hover_Test_Normals;
Vector3 averagedNormal = new Vector3();

for (int i = 0; i < hover_Test_Normals.Length; i++ )
{
averagedNormal[0] += hover_Test_Normals[i][0];
averagedNormal[1] += hover_Test_Normals[i][1];
averagedNormal[2] += hover_Test_Normals[i][2];
}

averagedNormal[0] = averagedNormal[0] / hover_Test_Normals.Length;
averagedNormal[1] = averagedNormal[1] / hover_Test_Normals.Length;
averagedNormal[2] = averagedNormal[2] / hover_Test_Normals.Length;

Quaternion rotateTo = base_Collider.transform.rotation;
rotateTo = Quaternion.FromToRotation(base_Collider.transform.up, averagedNormal) * rotateTo;

// Find vector of displacement from last frame
Vector3 displacmentSinceLastFrame = new Vector3(Mathf.Abs(base_Collider.transform.position.x - lastPosition.x), 0f, Mathf.Abs(base_Collider.transform.position.z - lastPosition.z));
Vector3 displacementDirection = displacmentSinceLastFrame.normalized;

var colliderStep = Time.deltaTime * groundMatchSpeed;
var modelStep = Time.deltaTime * turnSpeed;

// If grounded, mimic tilt of surface
if (tank_Manager.GetComponent<tank_State>().isGrounded == true)
{
transform.rotation = Quaternion.Lerp(rotation, rotateTo, colliderStep);
base_Model.transform.rotation = Quaternion.Slerp(rotation, rotateTo, colliderStep);
}

lastPosition = base_Collider.transform.position;
}

private Quaternion yRotation(Quaternion q)
{
float theta = Mathf.Atan2(q.y, q.w);

// quaternion representing rotation about the y axis
return new Quaternion(0, Mathf.Sin(theta), 0, Mathf.Cos(theta));
}


I would also like to voluntarily acknowledge I don't entirely understand Quaternions and may be doing something stupid as a result. If so, please let me know. Any other suggestions?

You can solve this with the method I show here: Camera-relative movement is pushing into/off the ground instead of parallel.

Vector3 CameraRelativeFlatten(Vector3 input, Vector3 localUp)
{
// It's more efficient to cache the reference to the camera to use, if you can.
Transform cam = Camera.main.transform;

// This first part creates a rotation looking (z+) along the terrain normal,
// "up" (y+) facing back toward the camera as closely as possible.
var zToUp = Quaternion.LookRotation(localUp, -cam.forward);

// This second part rotates the object's local y+ vector to face along z+,
// and takes z- to y+.
var yToZ = Quaternion.Euler(90, 0, 0);

// Chaining these, we get a rotation that maps local y+ to the terrain normal,
// and local z+ so it points away from the camera as best as possible.
var flatten zToUp * yToZ;

// Now we rotate our input vector into this frame of reference
return flatten * input;
}


This will map the z+ component of the input vector so that it points away from the camera, parallel to the terrain. The x+ will map to the camera's right, parallel to the terrain, and y+ will point perpendicular to the terrain.

You can turn this into a rotation with:

Quaternion.LookRotation(CameraRelativeFlatten(input, localUp), localUp);
`
• Thanks, I'll try implementing this when I get the time Nov 1 '21 at 22:32
• @SpermesePython don't forget to mark the answer as "Accepted" if it worked for you (by clicking the checkmark in the top-left), or update your question if you're still having problems. This helps ensure we don't have zombie questions that keep getting bumped by the Community bot without new updates. Dec 1 '21 at 17:07