Coordinate Frames
Why your imported scripts just work — and which way is "up" in the numbers you write.
Second Life and OpenSim describe the world one way; poqpoq's 3D engine (Babylon.js) describes it another. If those two never met, every imported script would point the wrong direction. They do meet — poqpoq translates between them — and this page explains the one rule you need to know so the numbers in your scripts always mean what you intend.
Two ways of saying "up"
| Second Life / OpenSim (LSL) | poqpoq native (Babylon) | |
|---|---|---|
| Up axis | Z is up (<0,0,1> points at the sky) |
Y is up ({x:0,y:1,z:0} points at the sky) |
| Handedness | Right-handed | Left-handed |
That difference is why a naive port would send a banner spinning on its side or fling an object sideways instead of upward. poqpoq closes the gap for you.
The rule: pick your frame by picking your language
If you write LSL
Write it exactly as you would in Second Life. Z
is up, rotations follow SL's convention, and a snippet pasted from the SL wiki
behaves the way the wiki says. poqpoq converts your coordinates to its native
frame at the boundary — you never see or think about it.
// ✅ LSL: up is Z, just like Second Life
llSetPos(llGetPos() + <0, 0, 1>); // moves the prim UP one metre
llTargetOmega(<0, 0, 1>, TWO_PI, 1.0); // spins it FLAT, around the vertical
If you write TypeScript
You're writing for poqpoq directly, so you're in poqpoq's native
frame: Y is up, left-handed, the same coordinates the
engine and the build tools use. Nothing is auto-converted — what you
write is what the world gets.
// ✅ TypeScript: up is Y, poqpoq-native
const p = await this.object.getPosition();
this.object.setPosition({ x: p.x, y: p.y + 1, z: p.z }); // UP one metre
Why convert instead of rewriting the script?
We translate at the moment a value crosses between your script and the engine — not by rewriting your code — for one decisive reason: scripts compute coordinates while they run.
llSetPos(llGetPos() + <0, 0, 1>); // the <0,0,1> is written down...
vector up = llRot2Up(llGetRot()); // ...but THIS up-vector is computed live
A one-time find-and-replace could only catch the vectors you typed literally, never the ones your script works out on the fly. Converting at the boundary handles both — and keeps your LSL faithful to its Second Life original.
Under the hood (for the curious)
You don't need this to write scripts — but if you're debugging a direction, here's what the translation actually does. Different kinds of value convert differently:
-
Positions & directions (where, which way, how fast,
pushes) — swap the
YandZcomponents. SL's "up"Zbecomes poqpoq's "up"Y. -
Spins & twists (
llTargetOmega, torque) — swap and flip the sign. Switching handedness reverses which way a rotation turns, so the spin axis is negated as well as swapped. (This is genuinely subtle — getting it wrong spins things backward.) -
Rotations (quaternions,
llGetRot/llSetRot) — neither a simple swap nor a flip; a handedness-aware transform that matches exactly how poqpoq placed your imported objects.
See also
- Getting Started — how LSL becomes TypeScript on poqpoq
- Healthy Scripts — keeping your creations smooth
- Function Reference — every LSL function and its TypeScript equivalent