Extract forward vector from camera matrix - c#

Scenario
I have a scene which contains several cameras, which may be facing any direction. I have no control over the creation of scene or cameras, but I need to work out which direction the cameras are facing in order to align them correctly.
The code below is a sample based on simple numbers, and I think shows that I'm missing something fundamental.
I am currently extracting the forward vector from the camera matrix using
Vector3 forward = new Vector3(-worldToCamera.M13, -worldToCamera.M23, -worldToCamera.M33);
as specified at
http://roy-t.nl/2010/03/04/getting-the-left-forward-and-back-vectors-from-a-view-matrix-directly.html
However, for any given rotated input, this always returns (0, 0, -1). My question is:
How do I get the forward direction in world space from the camera matrix?
Code
using System;
using System.Numerics;
public class Program
{
private const double OneEightyOverPI = 180d / Math.PI;
private const double TwoPI = Math.PI * 2d;
private const double PIOverTwo = Math.PI / 2d;
private const double ThreePIOverTwo = 3d * Math.PI / 2d;
public static void Main()
{
Vector3 cameraPosition = new Vector3(5, 5, 5);
Matrix4x4 translate = Matrix4x4.CreateTranslation(-cameraPosition);
Matrix4x4 cameraMatrix = translate;
Test(cameraMatrix, cameraPosition, 0);
// Z-axis is vertical, so rotate around it to change the pan angle of the camera
cameraMatrix = Matrix4x4.CreateRotationZ((float)PIOverTwo) * translate;
Test(cameraMatrix, cameraPosition, 90);
cameraMatrix = Matrix4x4.CreateRotationZ((float)Math.PI) * translate;
Test(cameraMatrix, cameraPosition, 180);
cameraMatrix = Matrix4x4.CreateRotationZ((float)ThreePIOverTwo) * translate;
Test(cameraMatrix, cameraPosition, 270);
}
private static void Test(Matrix4x4 worldToCamera, Vector3 cameraPositionWorld, int expected) {
// http://roy-t.nl/2010/03/04/getting-the-left-forward-and-back-vectors-from-a-view-matrix-directly.html
Vector3 forward = new Vector3(-worldToCamera.M13, -worldToCamera.M23, -worldToCamera.M33);
// input always aligned such that:
Vector3 north = Vector3.UnitY;
double angle = Math.Atan2(north.Y, north.X) - Math.Atan2(forward.Y, forward.X);
if (angle < 0) {
angle += TwoPI;
}
int deg = (int)(angle * OneEightyOverPI);
Console.WriteLine("Expected: " + expected + ", actual: " + deg + ", diff: " + (expected - deg));
}

Related

How to randomly find one of the vectors from point P tangent to a sphere

I would like to randomly select a vector originating at point P, such that the line formed alongside this vector is tangent to the surface of some sphere. To do this, I need a collection of all points S forming a circle on the sphere, such that the line SP is tangent to the sphere at point S. Then, given this information, I can select one point S', and create a direction vector from point P to point S'.
I would like to do this in Unity. I don't have much preference over how the vector is created itself, as long as it can be randomised and points to a point on the abovementioned circle. I believe an ideal solution would consider an angle θ ∈ [0, 2π] which can be randomised to give a vector from the origin of the circle (not the sphere) to the associated S' point.
I would appreciate a solution in C#, but I am happy with other languages too. Please do note that while the mathematical solutions are appreciated, I am specifically looking for implementation details, as I am not very fluent with Unity engine, their coordinate system and vector operations yet.
Visualisation below:
The solution in Unity is as follows:
Compute the circle's center point
Compute the circle's radius
Create a projection using a randomly selected point in 3D coordinates and point P
Use the center point and the radius to position the point, which results in finding S', such that PS' is tangent to the sphere
using UnityEngine;
public class Script : MonoBehaviour
{
private readonly float _accuracy = 0.1f;
private readonly int _vectorsCount = 1000;
private readonly Vector3 _point = new Vector3(600, 600, 600);
private readonly float _sphereRadius = 500f;
private void Start()
{
// This value will be used to calculate both the circle coordinates and its radius.
var quadraticSum = Mathf.Pow(_point.x, 2) + Mathf.Pow(_point.y, 2) + Mathf.Pow(_point.z, 2);
// Find out coordinates of the circle created by the intersection of the plane with the sphere
Vector3 circleCenter;
circleCenter.x = _point.x * Mathf.Pow(_sphereRadius, 2) / quadraticSum;
circleCenter.y = _point.y * Mathf.Pow(_sphereRadius, 2) / quadraticSum;
circleCenter.z = _point.z * Mathf.Pow(_sphereRadius, 2) / quadraticSum;
// Find out radius of the above circle
var circleRadius = _sphereRadius * Mathf.Sqrt(quadraticSum - Mathf.Pow(_sphereRadius, 2)) /
Mathf.Sqrt(quadraticSum);
/*
* At this point, we can start drawing - let's draw:
*
* - the point using red colour
* - the sphere using blue colour
* - the circle using green colour
*
* Below assumes center of the sphere is at (0, 0, 0)
*/
Debug.DrawLine(Vector3.zero, _point, Color.red, 1000);
DrawSphere();
DrawCircle(circleCenter, circleRadius);
}
private void DrawSphere()
{
for (var theta = -Mathf.PI; theta < Mathf.PI; theta += _accuracy)
{
for (var phi = -Mathf.PI; phi < Mathf.PI; phi += _accuracy)
{
var ray = new Vector3(
_sphereRadius * Mathf.Sin(theta) * Mathf.Cos(phi),
_sphereRadius * Mathf.Sin(theta) * Mathf.Sin(phi),
_sphereRadius * Mathf.Cos(theta)
);
Debug.DrawLine(Vector3.zero, ray, Color.blue, 1000);
}
}
}
private void DrawCircle(Vector3 center, float radius)
{
for (int i = 0; i < _vectorsCount; i++)
{
// Since I wanted random vectors, I am repeatedly drawing a random vector on the circle
var tangentPoint = Vector3.ProjectOnPlane(Random.insideUnitSphere, _point).normalized * radius + center;
Debug.DrawLine(Vector3.zero, tangentPoint, Color.green, 1000);
//Debug.DrawLine(_point, tangentPoint, Color.cyan, 1000);
}
}
}
See the screenshots for visualisation:
Here is an angle with the red line drawn after the sphere and the circle are drawn, to see the center of the sphere:

Unity 2D Fighting Game Camera

I'm trying to make a 2d fighting camera. I want the camera to focus on 2 fighters and then zoom in/out to make sure they are both on the screen. Using the post below, I was able to get this camera to work horizontally:
https://answers.unity.com/questions/126184/fighting-game-camera.html
The original script is the following
float margin = 1.5f; // space between screen border and nearest fighter
private float z0; // coord z of the fighters plane
private float zCam; // camera distance to the fighters plane
private float wScene; // scene width
private Transform f1; // fighter1 transform
private Transform f2; // fighter2 transform
private float xL; // left screen X coordinate
private float xR; // right screen X coordinate
public void calcScreen(Transform p1, Transform p2)
{
// Calculates the xL and xR screen coordinates
if (p1.position.x < p2.position.x)
{
xL = p1.position.x - margin;
xR = p2.position.x + margin;
}
else
{
xL = p2.position.x - margin;
xR = p1.position.x + margin;
}
}
public void Start()
{
// find references to the fighters
f1 = GameObject.Find("Fighter1").transform;
f2 = GameObject.Find("Fighter2").transform;
// initializes scene size and camera distance
calcScreen(f1, f2);
wScene = xR - xL;
zCam = transform.position.z - z0;
}
public void Update()
{
Vector3 pos = transform.position;
calcScreen(f1, f2);
float width = xR - xL;
if (width > wScene)
{ // if fighters too far adjust camera distance
pos.z = zCam * width / wScene + z0;
}
// centers the camera
pos.x = (xR + xL) / 2;
transform.position = pos;
}
Since on my game you can move horizontally and vertically, I modified the code to also adjust when the character moves vertically to this:
float margin = 1.5f; // space between screen border and nearest fighter
float marginY = 1.5f; // space between screen border and nearest fighter
private float z0; // coord z of the fighters plane
private float zCam; // camera distance to the fighters plane
private float wScene; // scene width
private float hScene; // scene height
private Transform f1; // fighter1 transform
private Transform f2; // fighter2 transform
private float xL; // left screen X coordinate
private float xR; // right screen X coordinate
private float yU; // up screen Y coordinate
private float yD; // down screen Y coordinate
public void calcScreen(Transform p1, Transform p2)
{
// Calculates the xL and xR screen coordinates
if (p1.position.x < p2.position.x)
{
xL = p1.position.x - margin;
xR = p2.position.x + margin;
}
else
{
xL = p2.position.x - margin;
xR = p1.position.x + margin;
}
if (p1.position.y < p2.position.y)
{
yD = p1.position.y - marginY;
yU = p2.position.y + marginY;
}
else
{
yD = p2.position.y - marginY;
yU = p1.position.y + marginY;
}
}
public void Start()
{
// find references to the fighters
f1 = GameObject.Find("Fighter1").transform;
f2 = GameObject.Find("Fighter2").transform;
// initializes scene size and camera distance
calcScreen(f1, f2);
wScene = xR - xL;
hScene = yU - yD;
zCam = transform.position.z - z0;
}
public void Update()
{
Vector3 pos = transform.position;
calcScreen(f1, f2);
float width = xR - xL;
float height = yU - yD;
if (width > wScene)
{ // if fighters too far adjust camera distance
pos.z = zCam * width / wScene + z0;
}
else if (height > hScene)
{ // if fighters too far adjust camera distance
pos.z = zCam * height / hScene + z0;
}
// centers the camera
pos.x = (xR + xL) / 2;
pos.y = (yU + yD) / 2;
transform.position = pos;
}
This works when the character only moves horizontally or vertically, if it moves both horizontally and vertically it doesn't work anymore. Also, the vertical movement doesn't work appropriately when the 2 fighters are far away. The camera zooms in even thought they are far from each other but they are close in terms of the height.
I feel like I need to change the code to look at both x and y at the same time. It feels like I'm considering and x separately.
I have been stuck on this for a while now. Any help would be appreciated.
Your else statement is the reason your vertical scaling is being removed. You should change your if block in Update to this.
if (width > wScene)
{ // if fighters too far adjust camera distance
pos.z = zCam * width / wScene + z0;
}
if (height > hScene)
{ // if fighters too far adjust camera distance
pos.z = Mathf.Max(zCam * height / hScene + z0, pos.z);
}

Instantiate in circle objects look to center

A little bit confused - math is really not my strong point, so I'm not sure how to achieve.
Currently the objects instantiate, but I have no control over the distance from center or the rotation of each spawned object
public void instantiateInCircle()
{
for (int i = 0; i < amount; i++)
{
float radius = spawnDistance;
float angle = i * Mathf.PI * 2f / radius;
Vector3 newPos = transform.position + (new Vector3(Mathf.Cos(angle) * radius, spawnHeight, Mathf.Sin(angle) * radius ));
//Rotate objects to look at the center
GameObject instantiatedObject = Instantiate(itemToSpawn, newPos, Quaternion.Euler(0, 0, 0));
instantiatedObject.transform.LookAt(spawnAroundThis.transform);
//How to adjust the width of the radius, how far away from the center?
//Parent instantiated objects to disk
instantiatedObject.transform.parent = spawnAroundThis.transform;
instantiatedObject.transform.localScale = new Vector3(scale, scale, scale);
}
}
How to make the distance adjustable, move cubes in closer to center...?
Currently, you do not access the instantiated object, but the prefab instead. Cache the object and call the LookAt on them.
Since I do not know what type itemToSpawn is, I assumed it is a GameObject. You may want to use your type instead.
GameObject instantiatedObject = Instantiate(itemToSpawn, newPos, Quaternion.Euler(0, 0, 0));
instantiatedObject.transform.LookAt(spawnAroundThis.transform);
If you want to control the distance from center of the rotation:
for (int i = 0; i < amount; i++)
{
float radius = spawnDistance;
float angle = i * Mathf.PI * 2f / (float)amount; // divide by amount, NOT radius
// manipulate radius here as you want
Vector3 newPos = transform.position + (new Vector3(Mathf.Cos(angle) * radius, spawnHeight, Mathf.Sin(angle) * radius ));
...
}

Converting radians to degrees with C#

I'm creating a small game in Unity with C#, I'm not allowed to use any of the built in physics or colliders. What I have is a platform with a weight object on it. With the arrow keys you can tilt the platforms angle. The weight on top should then react accordingly to the given tilt angle. I have calculated everything with the friction, max friction, normal force a.s.o. The thing that doesn't work is when you press a key to change the tilt, the force on the object should change accordingly, and if the force on the weight is greater that the maximum friction force, then the weight should move. Here is a simplified example of my code.
public float m = 35;
public float µ = 0.15f;
public float g = 9.82f; //(N/kg)
public float N;
public double deg = 0;
Fmax = m * g * µ;
if (Input.GetKeyDown(KeyCode.UpArrow)) {
deg++;
N = m * g * µ * (Math.Cos(deg) * (180.0 / Math.PI));
platform.transform.Rotate(0, 0, deg, Space.Self);
} else if (Input.GetKeyDown(KeyCode.DownArrow)) {
deg--;
N = m * g * µ * (Math.Cos(deg) * (180.0 / Math.PI));
platform.transform.Rotate(0, 0, deg, Space.Self);
}
if(N > Fmax) {
// Do the rest of the code
}
I think that the problem has to do with that Math.Cos(deg) turns the deg into a radian, but I tried to then covert back the reg to a deg with: (180.0 / Math.PI). I can get everything to work on my calculator but not in the code.
This is incorrect:
N = m * g * µ * (Math.Cos(deg) * (180.0 / Math.PI));
If the angle is in degrees you have to convert it to radians and pass that the cosine function:
double rad = deg*Math.PI/180.0;
N = m*g*mu*Math.Cos(rad);
You can easily convert from radians to degrees and from degrees to radians using the Mathf.Deg2Rad and Mathf.Rad2Deg constants
using UnityEngine;
public class Example : MonoBehaviour
{
// convert 1 radian to degrees
float rad = 10.0f;
void Start()
{
float deg = rad * Mathf.Rad2Deg;
Debug.Log(rad + " radians are equal to " + deg + " degrees.");
}
}
There is a super handy function called Mathf.Rad2Deg and Mathf.Deg2Rad. They convert radians to degrees and degrees to radians.

Screenshot from a rendertexture (non-main camera) to fit sprite (in 3D space) exactly - Unity

How do you adjust or otherwise set up a screenshot to have the exact dimensions of a particular sprite that is in 3D space (so, Gameobject with SpriteRenderer).
The render camera is set up directly above the sprite but has some blank space outside the boundaries of the sprite. I'm not sure how to precisely adjust the viewport of the rendercamera.
Here's what the scene looks like. (Actually this is a quick and simplified version of my current scene, to showcase my specific problem) And yes, I'm trying to take a specific screenshot at runtime. The main camera is at an arbitrary angle but the render camera can be centered to the sprite renderer. In other words, the screenshot I am taking looks like the one in the Camera Preview in bottom right corner in the pic below. How do I crop out (or adjust the viewport to exclude) the extra part beyond the size of the sprite renderer?
You can calculate the size of the frustum in world space:
float frustumHeight = 2.0f * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
float frustrumWidth = frustumHeight * camera.aspect;
Then if you know the painting's height and width in world units, you can calculate how big of the center piece of the screen it's occupying:
float widthPortion = paintingWidth / frustrumWidth;
float heightPortion = paintingHeight / frustrumHeight;
Then you know how much of the center of the renderTexture to hide:
float leftRightCrop = (1f-widthPortion)/2f;
float topBottomCrop = (1f-heightPortion)/2f;
So to copy it from your camera's render texture you can do this in OnPostRender:
RenderTexture currentRT = RenderTexture.active;
RenderTexture.active = Cam.targetTexture;
Cam.Render();
int croppedPixelWidth = widthPortion*camera.pixelWidth;
int croppedPixelHeight = heightPortion*camera.pixelHeight;
int leftPixelPadding = leftRightCrop*camera.pixelWidth;
int bottomPixelPadding = topBottomCrop*camera.pixelHeight;
Texture2D cropped = new Texture2D(croppedPixelWidth, croppedPixelHeight);
cropped.ReadPixels(new Rect(
leftPixelPadding,
bottomPixelPadding,
leftPixelPadding + croppedPixelWidth,
bottomPixelPadding + croppedPixelHeight), 0, 0);
cropped.Apply();
RenderTexture.active = currentRT;
Or as #ina used it,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class SpritePerfectScreenshot : MonoBehaviour
{
bool ScreenshotIt;
float widthPortion; float heightPortion;
float leftRightCrop; float topBottomCrop;
Camera camera;
public void TakeScreencap(Camera c,float distance,float pw,float ph)
{
camera = c;c.enabled = true;
SetItUp(distance, pw, ph);
}
void SetItUp(float distance,float paintingWidth,float paintingHeight)
{
float frustrumHeight = 2.0f * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
float frustrumWidth = frustrumHeight * camera.aspect;
widthPortion = (paintingWidth / frustrumWidth);
heightPortion = (paintingHeight / frustrumHeight);
leftRightCrop = (1f - widthPortion) / 2f;
topBottomCrop = (1f - heightPortion) / 2f;
ScreenshotIt = true;
print("SetItUp " + distance);
}
private void OnPostRender()
{
if (ScreenshotIt)
{
int croppedPixelWidth =(int) (widthPortion * camera.pixelWidth);
int croppedPixelHeight =(int) (heightPortion * camera.pixelHeight);
int leftPixelPadding =(int)( leftRightCrop * camera.pixelWidth);
int bottomPixelPadding =(int)( topBottomCrop * camera.pixelHeight);
print(croppedPixelWidth + " " + croppedPixelHeight);
Texture2D cropped = new Texture2D(croppedPixelWidth, croppedPixelHeight);
cropped.ReadPixels(new Rect(
leftPixelPadding,
bottomPixelPadding,
leftPixelPadding + croppedPixelWidth,
bottomPixelPadding + croppedPixelHeight), 0, 0);
cropped.Apply();
string uuid = UUID.GetUUID(); string localPath = Application.persistentDataPath + "/" + uuid + ".png";
#if !UNITY_EDITOR
NativeGallery.SaveImageToGallery(cropped.EncodeToPNG(), "A Beautiful Letter",uuid);
#else
File.WriteAllBytes(localPath,cropped.EncodeToPNG());
print(localPath);
#endif
//TODO server (?)
ScreenshotIt = false;camera.enabled = false;
}
}
}

Categories