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

One sentence to remember
LSL is always Second Life's frame. TypeScript is always poqpoq's frame. You choose which coordinate system your numbers live in simply by choosing which language you write in.

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
This is true whether or not the script was imported
It doesn't matter if your LSL arrived inside an OAR or you typed it fresh in the poqpoq editor — LSL is LSL. It always uses SL's Z-up frame, and poqpoq always converts it. So please don't "pre-correct" your vectors to Y-up; that double-converts them and sends things the wrong way.

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:

The contract behind this
These rules are ratified in ADR-149 (Scripting Coordinate-Frame Contract). The guiding promise: an LSL script behaves the way it did in Second Life, and a TypeScript script behaves the way poqpoq natively works — with one consistent translation layer in between.

See also