Jump to content

2.21 How to correctly use new converters API?


photo

Recommended Posts

Posted

Hello,

For debugging incoming PDU, I need to manually draw some points and directions when receiving entities PDU. But I can't make correct use of the new API. For example, here I'm just trying to draw the XYZ frame of reference of a processed entity, but the frame drawn is never the correct one. I must be doing something wrong...

void WorldLogic::on_process_Entity_State_PDU(KDIS::PDU::Header const* packet)
{
	auto pdu = static_cast<KDIS::PDU::Entity_State_PDU const*>(packet);
	auto ecef0 = pdu->GetEntityLocation();
  
	auto geo0 = ig->getConverter()->geocentricToGeodetic({ ecef0.GetX(), ecef0.GetY(), ecef0.GetZ() });
	auto w0 = ig->getConverter()->geodeticToWorld(geo0); //< works OK
  
	auto ecefdir0 = pdu->GetEntityOrientation();
	auto euler0 = ig->getConverter()->geocentricEulerToGeodeticEuler(geo0, { ecefdir0.GetPsiInRadians(), ecefdir0.GetThetaInRadians(), ecefdir0.GetPhiInDegrees() });
	// from doc of geocentricEulerToGeodeticEuler:
	//(Psi, Theta, Phi) in radians — yaw about Z, then pitch about Y, then roll about X
	//Euler angles in the local geodetic (NED) frame as (pitch, roll, heading) in radians.
	
	euler0 *= 180 / 3.14159265; // THE API produces radians but consumes degrees....
  	// NONE of these two lines works, and they don't produce the same result!
	auto quat0 = ig->getConverter()->geodeticEulerToRotation(geo0, { euler0.x, euler0.y, euler0.z });// from doc: Euler angles in degrees as (pitch, roll, heading) 
	//auto quat0 = ig->getNEDConverter()->eulerNEDToRotation({ euler0.x, euler0.y, euler0.z });
  
	// transform to world (can you confirm it's necessary here?)
	auto wquat0 = ig->getConverter()->getZeroRotation(geo0) * quat0;

	auto dir0 = wquat0 * vec3{ 1,0,0 };
	auto dir1 = wquat0 * vec3{ 0,1,0 };
	auto dir2 = wquat0 * vec3{ 0,0,1 };
	Visualizer::renderPoint3D(w0, 0.06f, vec4_blue, true, 0, false);
	Visualizer::renderDirection(w0, dir0, vec4_red, 0.25f, true, 0, false);
	Visualizer::renderDirection(w0, dir1, vec4_green, 0.25f, true, 0, false);
	Visualizer::renderDirection(w0, dir2, vec4_blue, 0.25f, true, 0, false);
	// I would expect this frame to be the exact same as the frame shown with ig_debug_entity_info 1 (ignoring interpolation)
}

I have to point that all Euler and various angles API are very misleading: sometimes it's radian, sometimes degree, sometimes the order of rotation is not explicit...

It would greatly help if at least the name of input variables was clear and updated in the doc and #include:

Math::vec3 geocentricEulerToGeodeticEuler ( const Math::dvec3 & geodetic_coordinate, const Math::vec3 & geocentric_euler ) const
  =>
Math::vec3 geocentricEulerToGeodeticEuler ( const Math::dvec3 & geodetic_lat_lon_alt_deg, const Math::vec3 & geocentric_euler_psi_theta_phi_rad ) const
  

(yes, it's very verbose, but at least in the code you know what to expect)

Another suggestion: add a lot more code samples in the doc for these API.

Thanks!

Posted

Hello! 
There are few independent bugs in the code — any one of them is enough to break the frame. 

1. GetPhiInDegrees (main problem)

{ ecefdir0.GetPsiInRadians(), ecefdir0.GetThetaInRadians(), ecefdir0.GetPhiInDegrees() }
//                                                                    ^^^^^^^^^

All three DIS orientation angles are in radians

2. geodeticEulerToRotation already returns a world-space quaternion

quat Converter::geodeticEulerToRotation(const dvec3 &geo, const vec3 &euler) const {
    return getZeroRotation(geo) * eulerToRotation(euler);
}

This line:

auto wquat0 = ig->getConverter()->getZeroRotation(geo0) * quat0;

rotates it a second time. Direct answer to your question: no, you should not transform into world space again. The commented-out variant using eulerNEDToRotation does NOT apply getZeroRotation, which is why after the extra multiplication the two branches disagree.

3. Order is consistent everywhere: (pitch, roll, heading) == (x, y, z). Your rad→deg conversion is correct. 

 

On your API feedback: You're right — mixing rad/deg across adjacent methods is genuinely confusing. It's worth correcting the names of the arguments so that they clearly indicate the units of measurement.

Posted
On 4/20/2026 at 8:13 AM, cash-metall said:

GetPhiInDegrees (main problem)

Doh! Stupid auto complete and out of screen error. Thanks!

On 4/20/2026 at 8:13 AM, cash-metall said:

geodeticEulerToRotation already returns a world-space quaternion

I would suggest using a more explicit naming convention, like geodeticEulerToWorldRotation, and renaming eulerToRotation to eulerToLocalRotation, and eulerNEDToRotation to eulerNEDToLocalRotation. Otherwise, it is impossible to understand from signature what is the frame of reference (Local, World, ECEF). It's already hard enough to manipulate all this...

But now, thanks to your explanation, I have the correct frame of reference! Thanks!

Posted

I thought I had it, but noooo, it doesn't work / I don't understand.

// draw the world rotation frame of reference 'wq' at world position 'wpos'
static void renderFrame(Vec3 wpos, quat wq, float size = 0.25f, bool screen_space = false, float duration = 0.0f, bool depth_test = true) {
	auto dir0 = wq * vec3{ 1,0,0 };
	auto dir1 = wq * vec3{ 0,1,0 };
	auto dir2 = wq * vec3{ 0,0,1 };
	Visualizer::renderPoint3D(wpos, 0.06f, vec4_white, screen_space, duration, depth_test);
	Visualizer::renderDirection(wpos, dir0, vec4_red, size, screen_space, duration, depth_test);
	Visualizer::renderDirection(wpos, dir1, vec4_green, size, screen_space, duration, depth_test);
	Visualizer::renderDirection(wpos, dir2, vec4_blue, size, screen_space, duration, depth_test);
}
// draw CIGI world frame of reference
void WorldLogic::on_process_entity_control(IG::CIGI::CigiHostPacket const* p)
{
	auto packet = dynamic_cast<IG::CIGI::CigiEntityControl const*>(p);
	if (packet->getAttachState() == 1)
		return;
	auto geo = packet->getPosition();
	auto rpy = packet->getRotation(); // CIGI Roll Pitch Yaw in degrees

	auto w0 = ig->getConverter()->geodeticToWorld(geo);
	auto wquat0 = ig->getConverter()->geodeticEulerToRotation(geo, { rpy.y, rpy.x, rpy.z });//Euler angles in degrees as (pitch, roll, heading) 

	renderFrame(w0, wquat0, 0.5f, true, 0, false);
}
// draw DIS world frame of reference
void WorldLogic::on_process_Entity_State_PDU(KDIS::PDU::Header const* packet)
{
	auto pdu = static_cast<KDIS::PDU::Entity_State_PDU const*>(packet);
	auto ecef0 = pdu->GetEntityLocation();
	auto geo0 = ig->getConverter()->geocentricToGeodetic({ ecef0.GetX(), ecef0.GetY(), ecef0.GetZ() });
	auto w0 = ig->getConverter()->geodeticToWorld(geo0);

	auto ecefdir0 = pdu->GetEntityOrientation();
	auto euler0 = ig->getConverter()->geocentricEulerToGeodeticEuler(geo0, 
                  { ecefdir0.GetPsiInRadians(), ecefdir0.GetThetaInRadians(), ecefdir0.GetPhiInRadians() });
	//(Psi, Theta, Phi) in radians — yaw about Z, then pitch about Y, then roll about X
	//Euler angles in the local geodetic (NED) frame as (pitch, roll, heading) in radians.
	euler0 *= 180 / 3.14159265;

	auto wquat0 = ig->getConverter()->geodeticEulerToRotation(geo0, { euler0.x, euler0.y, euler0.z });
	//Euler angles in degrees as (pitch, roll, heading) 

	renderFrame(w0, quat0, 0.5f, true, 0, false);
}

Here I want to draw the frame of reference for CIGI and DIS entities as they are received. None of these works. I'm definitely lost. What am I missing?

Posted

Hello! 

Main problem that Converter::geodeticEulerToRotation use ENU asix orientation, but IG use NED by default (CIGI-style)

for entity IG do:

Mat4 transform = Mat4(local_rot, local_pos);
transform = Mat4(ltp_rot, ltp_pos) * transform;   // world = ltp * local

where ltp_rot — this is getZeroRotation(geo), and local_rot calculated by NEDConverter :

local_rot = ig->getNEDConverter()->eulerTARGETToRotation(euler);

 

so the final code should be like 

static void renderFrame(Vec3 wpos, quat wq, float size = 0.25f,
                        bool screen_space = true, float duration = 0.0f, bool depth_test = true)
{
    Visualizer::renderPoint3D (wpos, 0.06f, vec4_white, screen_space, duration, depth_test);
    Visualizer::renderDirection(wpos, wq * vec3{1,0,0}, vec4_red,   size, screen_space, duration, depth_test);
    Visualizer::renderDirection(wpos, wq * vec3{0,1,0}, vec4_green, size, screen_space, duration, depth_test);
    Visualizer::renderDirection(wpos, wq * vec3{0,0,1}, vec4_blue,  size, screen_space, duration, depth_test);
}

// CIGI — same path as CIGIConnector::entity_control
void WorldLogic::on_process_entity_control(IG::CIGI::CigiHostPacket const* p)
{
    auto packet = dynamic_cast<IG::CIGI::CigiEntityControl const*>(p);
    if (packet->getAttachState() == 1) return;

    auto geo = packet->getPosition();
    auto rpy = packet->getRotation();   // (roll, pitch, yaw) in deg — pass through as-is

    auto w   = ig->getConverter()->geodeticToWorld(geo);
    auto loc = ig->getNEDConverter()->eulerTARGETToRotation(rpy);   // same op engine uses
    auto wq  = ig->getConverter()->getZeroRotation(geo) * loc;

    renderFrame(w, wq, 0.5f);
}

// DIS — same path as DISConnector::Entity_State_PDU
void WorldLogic::on_process_Entity_State_PDU(KDIS::PDU::Header const* packet)
{
    auto pdu  = static_cast<KDIS::PDU::Entity_State_PDU const*>(packet);
    auto ecef = pdu->GetEntityLocation();
    auto geo  = ig->getConverter()->geocentricToGeodetic({ecef.GetX(), ecef.GetY(), ecef.GetZ()});
    auto w    = ig->getConverter()->geodeticToWorld(geo);

    auto o = pdu->GetEntityOrientation();
    // geocentricEulerToGeodeticEuler returns (pitch, roll, heading) in radians
    vec3 e_rad = ig->getConverter()->geocentricEulerToGeodeticEuler(
        geo, { o.GetPsiInRadians(), o.GetThetaInRadians(), o.GetPhiInRadians() });
    vec3 e_deg = e_rad * (180.0f / 3.14159265f);

    // Use NED path (not geodeticEulerToRotation), to match what entity->setRotationEuler does
    auto loc = ig->getNEDConverter()->eulerTARGETToRotation(e_deg);
    auto wq  = ig->getConverter()->getZeroRotation(geo) * loc;

    renderFrame(w, wq, 0.5f);
}

 

Methods moved into GeodeticConverter operate in the coordinate frame adopted in Unigine: X-right, Y-forward, Z-up. Therefore, for the IG plugin, we separated the axis-swapping logic into NEDConverter to avoid confusing other users. Maybe NEDConverter should have its own analogue of geodeticEulerToRotation

  • Like 1
Posted

Now it works! Thanks a lot for the explanation. I understand naming angles with all these frames and convention is not an easy task, but I think some better labelling would help, less concise and more explicit, maybe more code samples directly in the doc. The name TARGET for example is really not clear to me in eulerTARGETToRotation, what is it referring to? I understand geocentricEuler angles, but now I'm confused about what is geodeticEuler?

Posted

Hi Stephane,

We are currently working on a geodetic converter article, hope it would be available soon 🙂

About target: you can define target by calling setTargetSystem() where you can select which system you need (NED / ENU) and *toTarget* will use it.

Thank you!

  • Thanks 1

How to submit a good bug report
---
FTP server for test scenes and user uploads:

Posted

@cash-metall One more question: I tried to do the inverse of the previous code and couldn't wrap my head around how to do this:

How can you convert from a Unigine node WorldPotision and WorldRotation to an entity GeoPosition and Euler Rotation, so I can manually place an entity exactly over some node? (I always end with some funky rotation in all my tests)

Thanks!

Posted

The inverse transform is a mirror: Entity::setGeoPosition + Entity::setRotationEuler. 

Logic:
 

// Given: a node (or any world-space transform) → get geodetic position + euler rotation
// that would produce the same result if passed to entity->setGeoPosition / setRotationEuler

static void worldTransformToGeo(
    IG::Manager* ig,
    const Mat4& world_transform,       // e.g. node->getWorldTransform()
    dvec3& out_geo,                     // (lat, lon, alt) degrees
    vec3& out_euler_deg)                // euler in same convention as setRotationEuler
{
    // --- position: world → geodetic ---
    Vec3 wpos = world_transform.getTranslate();
    out_geo = ig->getConverter()->worldToGeodetic(wpos);

    // --- rotation: world quat → local euler ---
    // Entity::update_transform does: world = Mat4(ltp_rot, ltp_pos) * Mat4(local_rot, local_pos)
    // So: local_rot = inverse(ltp_rot) * world_rot
    // Where ltp_rot ≈ getZeroRotation(geo)

    quat world_rot = world_transform.getRotate();
    quat ltp_rot   = ig->getConverter()->getZeroRotation(out_geo);
    quat local_rot = inverse(ltp_rot) * world_rot;

    // Inverse of eulerTARGETToRotation — matches NED/ENU config
    out_euler_deg = ig->getNEDConverter()->rotationToEulerTARGET(local_rot);
}


Usage:

// from an existing node
dvec3 geo;
vec3 euler_deg;
worldTransformToGeo(ig->getManager(), node->getWorldTransform(), geo, euler_deg);

// verify round-trip: these should reproduce the original world transform
auto w   = ig->getConverter()->geodeticToWorld(geo);
auto loc = ig->getNEDConverter()->eulerTARGETToRotation(euler_deg);
auto wq  = ig->getConverter()->getZeroRotation(geo) * loc;
renderFrame(w, wq, 0.5f);  // should match node's frame exactly

To convert back to  DIS ECEF euler (Psi/Theta/Phi):

// geodetic euler (degrees) → geocentric euler (radians) for DIS PDU
vec3 geocentric_euler_rad = ig->getConverter()->geodeticEulerToGeocentricEuler(geo, euler_deg);
// geocentric_euler_rad = (psi, theta, phi) in radians — ready for DIS Entity_State_PDU

The chain: getZeroRotation и eulerTARGETToRotation / rotationToEulerTARGET — are strictly inverce to each other, so round-trip does not cause any losses (except for floating-point precision).

If you have some code snippet that doesn't work for you, please let me know.

  • Like 1
Posted

I had something very similar, and your code produces exactly the same result for me: the pitch rotation is off by 90°. 

This code was used in 2.20 and earlier to be able to manually "fly" the entity 0 with the Free camera mode from the master. Here is the code now in 2.21:

int IGSystemLogic::postUpdate() {
	if (ig->isMaster() && ig->isDebugEnabled()) {
		// Detect activation of the IG debug player
		if (!Console::isActive()) {
			if (Input::isKeyDown(Input::KEY_F))
				is_debug_player = true;
			if (Input::isKeyDown(Input::KEY_G))
				is_debug_player = false;
		}
		if (ig_debug_fly.get() && is_debug_player) {
			auto e = ig->getEntity(0);
			Vec3 geo;
			vec3 euler;
			worldTransformToGeo(ig, Game::getPlayer()->getWorldTransform(), geo, euler);
			e->setInterpolation(false);
			e->setGeoPosition(geo);
			e->setRotationEuler(euler);
		}
	}
}

When testing it, just activate ig_debug 1 and ig_debug_fly 1 (a custom cvar here) and press F: the entity 0 is supposed to replicate the position & orientation of the Free camera, but it appears with the wrong pitch. What did I do wrong?

Posted

yes.

for camera it should be 

worldTransformToGeo(ig, Game::getPlayer()->getWorldTransform() * rotateX(-90.0), geo, euler);

 

  • Thanks 1
Posted

Ooooh of course! Not the first time this bites me, but I keep forgetting! Thank you again for helping on this!

  • Like 1
×
×
  • Create New...