What you want to achieve is doable using a custom projection matrix. The one we used must be parallelepipedoidal, as mentioned in my comment earlier. To picture it, first look at an orthogonal camera frustrum in Unity (that white box that appears in front of the camera when you select it and its projection setting is set to “orthogonal”). Now, imagine you take the far side of that box and drag it down. Here is a quick drawing illustrating the concept:

You have a 3d object (blue square) and a sprite (blue line) illustrated. The dotted grey lines are how vertices are projected upon the screen depending on if you use an orthogonal or custom projection matrix. As you can see, with an ortogonal matrix, you wouldn’t see the floor at all, or any flat surface parallel with the X-Z plane. With the custom matrix, not only you can see those surfaces (notice how the 3d object is projected) but every vertical surface is still seen as vertical. This means that your world can be as it is supposed to be, without strange rotation hacks, and you can still see stuff as you want to see it.
The kind of result I got with three planes and a sprite (purposefully going through planes to show it has normal rotation values):

And this would be my projection matrix for the above screenshot:
0.18 0 0 0
0 0.1 0 0
0 -0.04 -0.02 0
0 0 0 1
You can instantiate a Matrix4x4, use SetRow(), SetColumn() or give your matrix the [SerializeField] attribute and assign it these values, then tweak them to get the intended result. Then you can assign that matrix to the projectionMatrix property of your camera.
The key value in the matrix is that -0.04. This is what allows for an isometric view with a perfectly horizontal camera. It’s a way of using the Z value of a given point to affect the Y value where it will be projected.
[EDIT]
I made these two simple files you could use when you are ready.
The first one is to be put on your camera game object as a component. It exposes a matrix and updates the camera’s projection matrix every frame.
ProjectionMatrixTester.cs
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class ProjectionMatrixTester : MonoBehaviour
{
Camera camera;
[SerializeField] Matrix4x4 matrix;
void Start()
{
camera = GetComponent<Camera>();
InitializeMatrix();
}
void Update()
{
camera.projectionMatrix = matrix;
}
void InitializeMatrix()
{
matrix.SetRow(0, new Vector4(0.09f, 0, 0, 0));
matrix.SetRow(1, new Vector4(0, 0.05f, -0.02f, 0));
matrix.SetRow(2, new Vector4(0, 0, -0.02f, -1));
matrix.SetRow(3, new Vector4(0, 0, 0, 1));
}
}
This second file changes the way the matrix is shown in the inspector. It shows it as it is supposed to be. It has to be placed in a folder named Editor.
MatrixPropertyDrawer.cs
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(Matrix4x4))]
public class MatrixPropertyDrawer : PropertyDrawer
{
const float CELL_WIDTH = 48;
const float CELL_HEIGHT = 16;
Rect position;
SerializedProperty property;
GUIContent label;
public override void OnGUI(Rect pos, SerializedProperty prop, GUIContent lab)
{
position = pos;
property = prop;
label = lab;
EditorGUI.BeginProperty(position, label, property);
DrawLabel();
DrawMatrix();
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty p, GUIContent l)
{
return 5 * CELL_HEIGHT;
}
void DrawLabel()
{
EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
position.y += CELL_HEIGHT;
EditorGUI.indentLevel = 0;
}
void DrawMatrix()
{
for (int r = 0; r < 4; r++)
{
for (int c = 0; c < 4; c++)
{
DrawCell(c, r);
}
}
}
void DrawCell(int column, int row)
{
Vector2 cellPos = position.position;
cellPos.x += CELL_WIDTH * column;
cellPos.y += CELL_HEIGHT * row;
EditorGUI.PropertyField(
new Rect(cellPos, new Vector2(CELL_WIDTH, CELL_HEIGHT)),
property.FindPropertyRelative("e" + column + row),
GUIContent.none
);
}
}