diff --git a/_includes/homepage/post-playlist.html b/_includes/homepage/post-playlist.html index e0909e66..88358d6e 100644 --- a/_includes/homepage/post-playlist.html +++ b/_includes/homepage/post-playlist.html @@ -26,9 +26,9 @@ {%- endif -%} {%- if include.tag and include.tag != "" -%} - {%- assign filtered_pages = site.pages | where_exp: "page", "page.tags contains include.tag" -%} + {%- assign filtered_pages = site.pages | where_exp: "page", "page.layout == 'post' and page.permalink and page.hidden != true and page.tags contains include.tag" -%} {%- else -%} - {%- assign filtered_pages = site.pages -%} + {%- assign filtered_pages = site.pages | where_exp: "page", "page.layout == 'post' and page.permalink and page.hidden != true" -%} {%- endif -%} {%- assign filtered_pages = filtered_pages | sort: "updatedAt" | reverse -%} @@ -42,9 +42,7 @@
{{ heading }}
{%- for relevant_page in filtered_pages limit: limit -%} - {%- if relevant_page.layout == "post" and relevant_page.permalink -%} - {% include post-discovery/post-card.html post=relevant_page classes="" %} - {%- endif -%} + {% include post-discovery/post-card.html post=relevant_page classes="" %} {%- endfor -%}
diff --git a/_includes/post-discovery/post-card.html b/_includes/post-discovery/post-card.html index cef1d2ce..a9fdd0bb 100644 --- a/_includes/post-discovery/post-card.html +++ b/_includes/post-discovery/post-card.html @@ -18,7 +18,11 @@ {% assign generated_placeholder_slug = include.post.permalink | replace: '.html', '' | slugify %} {% assign generated_placeholder_path = '/public/generated/placeholders/' | append: generated_placeholder_slug | append: '.jpg' %} {% assign generated_placeholder_file = site.static_files | where: 'path', generated_placeholder_path | first %} -{% if include.hidden %} +{% assign hide_card = include.post.hidden %} +{% if include.hidden == true %} + {% assign hide_card = true %} +{% endif %} +{% if hide_card == true %} {% else %}
diff --git a/_includes/posts/post-header.html b/_includes/posts/post-header.html index 12d2101f..beca0663 100644 --- a/_includes/posts/post-header.html +++ b/_includes/posts/post-header.html @@ -12,11 +12,11 @@ {% endif %}
{% if page.thumbnail %} - Post thumbnail + Post thumbnail {% elsif page_category_meta and page_category_meta.image %} - {{ page_category_meta.title | default: page_category }} + {{ page_category_meta.title | default: page_category }} {% else %} - Retro reversing logo + Retro reversing logo {% endif %}
diff --git a/_includes/recommended-sidebar.html b/_includes/recommended-sidebar.html index 52d64398..99dc27ac 100644 --- a/_includes/recommended-sidebar.html +++ b/_includes/recommended-sidebar.html @@ -15,7 +15,7 @@ {% endcomment %} {% for recommend in related_topics %} - {% for post in site.pages where_exp:"post","post.tags contains recommend" %} + {% for post in site.pages where_exp:"post","post.tags contains recommend and post.hidden != true" %} {% if post.tags contains recommend and post.url != include.current_url %}
  • {% include_cached post-discovery/post-card.html post=post classes="" %} @@ -30,7 +30,7 @@ This is for when the related topics are NOT an array {% endcomment %} - {% for post in site.pages where_exp:"post","post.tags contains related_topics" %} + {% for post in site.pages where_exp:"post","post.tags contains related_topics and post.hidden != true" %} {% if post.tags contains related_topics and post.url != include.current_url %}
  • {% include_cached post-discovery/post-card.html post=post classes="" %} diff --git a/pages/general/maths/Matrix.md b/pages/general/maths/Matrix.md index 5e2a9871..8f0333f4 100644 --- a/pages/general/maths/Matrix.md +++ b/pages/general/maths/Matrix.md @@ -1,48 +1,265 @@ --- layout: post -tags: +tags: - maths -title: Matrices/Matrix (Maths for Game Developers) -category: maths +- psp +- ds +title: Matrices (Maths for Game Developers) +category: +- maths +- ds +- psp permalink: /Matrix breadcrumbs: - name: Home url: / + - name: Introduction + url: /introduction - name: Maths for Game Developers url: /maths - - name: Matrices/Matrix + - name: Matrices url: # -recommend: +recommend: - maths - sdk - introduction -editlink: /articles/maths/Matrix.md +- psp +- ds +editlink: /pages/general/maths/Matrix.md +updatedAt: '2026-04-25' --- -# Introduction to matrices -A Matrix is just a table of numbers, thats all it is, really! By puttting numbers in a table like this we can do some very cool stuff which really shines when doing 3D game programming or even AI. +# Introduction to Matrices +A matrix is a table of numbers, but in game code the important part is what that table does [^1]. +Matrices let engines package translation, rotation, scale, camera transforms, and projection into a form that composes cleanly and applies efficiently to many points at once [^1][^2]. + +Matrices in games usually show up in three common shapes: +* **`3x3`** - Useful for rotation, scale, and basis changes when no translation is needed +* **`4x4`** - The standard choice for full 3D transforms because it can also encode translation and projection through homogeneous coordinates [^1] +* **`4x3`** - A compact affine form used by some SDKs, including the Nintendo DS, when projection is handled separately + +Matrices are useful for: +* **Model transforms** - Move a mesh from local space into world space +* **Camera transforms** - Move the world into camera space +* **Projection** - Convert 3D positions into clip or screen-friendly space +* **Composition** - Combine several transforms into one reusable matrix +* **Batch processing** - Apply one transform to many vertices, bones, or collision points + +Khan Academy's introduction to matrices is a useful warm-up if you want a quick visual refresher on what matrices are and how to read them: + + + +If vectors are still new, it helps to read that page first because matrix math builds directly on vector operations: +{% include_cached link-to-other-post.html post="/Vectors" description="Vectors are the building blocks that matrices transform, combine, and project." %} + +--- +## Core Matrix Concepts +### Identity matrix +The identity matrix is the matrix equivalent of multiplying by `1`. +Applying it leaves a vector unchanged, which is why it is the default starting point for many transform pipelines [^1]. + +This is the smallest useful example: + +```ts +type Vec2 = { x: number; y: number }; +type Mat2 = [[number, number], [number, number]]; + +function multiplyMat2Vec2(m: Mat2, v: Vec2): Vec2 { + return { + x: m[0][0] * v.x + m[0][1] * v.y, + y: m[1][0] * v.x + m[1][1] * v.y, + }; +} + +const identity: Mat2 = [ + [1, 0], + [0, 1], +]; + +const velocity = { x: 3, y: -2 }; +const unchanged = multiplyMat2Vec2(identity, velocity); +``` + +In this example `unchanged` is still `{ x: 3, y: -2 }`. +That sounds trivial, but it matters because identity matrices are the neutral element you compose other transforms onto. + +### Matrix-vector multiplication +The most important matrix operation in games is multiplying a matrix by a point or direction. +That is how a local-space vertex becomes a world-space vertex, or how a world-space point becomes a camera-space point [^1][^3]. + +This small 2D homogeneous-coordinate example shows a translation: + +```ts +type Vec3 = { x: number; y: number; w: number }; +type Mat3 = [ + [number, number, number], + [number, number, number], + [number, number, number], +]; + +function multiplyMat3Vec3(m: Mat3, v: Vec3): Vec3 { + return { + x: m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.w, + y: m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.w, + w: m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.w, + }; +} + +const translateRightBy3: Mat3 = [ + [1, 0, 3], + [0, 1, 0], + [0, 0, 1], +]; + +const point = { x: 2, y: 1, w: 1 }; +const moved = multiplyMat3Vec3(translateRightBy3, point); +``` + +In this example `moved` becomes `{ x: 5, y: 1, w: 1 }`. +The extra `w` lane is what lets a matrix encode translation instead of only rotation and scale. + +### Composition and order +One of the biggest practical lessons with matrices is that transform order matters [^2]. +Scaling, then rotating, then translating is not the same as translating first and scaling afterward. + +This example shows why: + +```ts +type Mat3 = [ + [number, number, number], + [number, number, number], + [number, number, number], +]; + +type Vec3 = { x: number; y: number; w: number }; + +function multiplyMat3(a: Mat3, b: Mat3): Mat3 { + return [ + [ + a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0], + a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1], + a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2], + ], + [ + a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0], + a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1], + a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2], + ], + [ + a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0], + a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1], + a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2], + ], + ]; +} + +function multiplyMat3Vec3(m: Mat3, v: Vec3): Vec3 { + return { + x: m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.w, + y: m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.w, + w: m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.w, + }; +} + +const scaleX2: Mat3 = [ + [2, 0, 0], + [0, 1, 0], + [0, 0, 1], +]; + +const translateRightBy5: Mat3 = [ + [1, 0, 5], + [0, 1, 0], + [0, 0, 1], +]; + +const point = { x: 1, y: 0, w: 1 }; + +const scaleThenMove = multiplyMat3Vec3(multiplyMat3(translateRightBy5, scaleX2), point); +const moveThenScale = multiplyMat3Vec3(multiplyMat3(scaleX2, translateRightBy5), point); +``` + +In this example `scaleThenMove` becomes `{ x: 7, y: 0, w: 1 }`, while `moveThenScale` becomes `{ x: 12, y: 0, w: 1 }`. +That is why APIs that expose `Before` and `After` variants are useful: they make the ordering explicit instead of forcing you to guess. + +### 3x3, 4x3, 4x4, and homogeneous coordinates +In game math, matrix size usually tells you what kind of transform is being represented: +* **`3x3`** - Rotation, scale, or basis conversion only +* **`4x3`** - Affine 3D transform, usually rotation/scale plus translation, but not full projective math +* **`4x4`** - Full transform matrix, including projection [^1] + +The usual trick behind `4x4` matrices is homogeneous coordinates [^1]. +Points get a final coordinate of `w = 1`, while pure directions use `w = 0`. +That distinction is what makes translation affect positions but not affect direction vectors such as normals, velocity axes, or camera basis directions. + +### Transpose +Transposing a matrix swaps rows and columns [^4]. +That sounds simple, but it matters because engines and APIs often disagree about whether basis vectors live in rows or columns, or whether matrices are meant to be read left-to-right or right-to-left. +That row-major versus column-major confusion is one of the main reasons matrix code can feel inconsistent across engines even when the underlying math is the same. + +For example: + +```text +1 2 3 1 4 7 +4 5 6 -> 2 5 8 +7 8 9 3 6 9 +``` + +In graphics code, transpose often appears when converting between storage conventions or when preparing data for another API that expects the opposite orientation [^4]. + +### View and projection matrices +Once you move past model transforms, matrices also define how the camera sees the world: +* **View matrix** - Moves world-space points into camera space, often through a `LookAt` helper +* **Perspective matrix** - Makes distant objects appear smaller and defines a frustum with a field of view, aspect ratio, and near/far clip planes [^1][^5] +* **Orthographic matrix** - Preserves parallel lines and does not apply perspective foreshortening [^1] +* **Frustum matrix** - Encodes an off-centre viewing volume directly, which is useful in lower-level rendering APIs + +Here is a small `LookAt`-style example showing the intent of a view matrix: + +```ts +const eye = { x: 0, y: 2, z: 5 }; +const target = { x: 0, y: 0, z: 0 }; +const up = { x: 0, y: 1, z: 0 }; + +// A real LookAt helper would build orthonormal axes from these values +// and produce a view matrix that transforms world-space points into +// camera-space coordinates. +``` - +In this setup the camera sits slightly above and behind the origin, looks toward the world origin, and uses positive `Y` as its up direction. +That is exactly the kind of input later SDK helpers such as `MTX_LookAt` on DS or `sceVfpuLookAtMatrix` on PSP are designed to consume. -Things you can do or represent with Matrices: -* Add/subtract/multiply 2 together -* Transpose/Scale/Rotate Matrices -* LookAt Matrix -* Perspective Matrix -* Identity Matrix -* Frustum +The video below provides a visual explanation of how a perspective matrix turns a viewing frustum into clip-space coordinates and why field of view, aspect ratio, and clip planes all matter: -A Vector is basically a Matrix with just 1 row, you can find out more about Vectors in our post on them. + -## Perspective Matrix - +If you want to continue from matrices into rotation-specific representations, the Quaternion page is the natural next step: +{% include_cached link-to-other-post.html post="/Quaternions" description="Quaternions solve many of the rotation problems that become awkward with matrix-only or Euler-only workflows." %} +Matrices can represent rotation perfectly well, but quaternions are often preferred when you need stable interpolation between orientations or want to avoid some of the bookkeeping problems that come with repeated Euler-style rotations. --- -# Nintendo DS -The Nintendo DS Operating System has a basic Matrix library defined by the header file **IrisMTX.h**. This file was leaked as part of the September 2020 "Platinum leak" as it is part of the Nintendo DS Boot ROM. +## Matrix Libraries used in Retail Console Game Development +Looking at retail SDK headers is useful because it shows which matrix operations console programmers expected to use frequently. + +### Nintendo DS Official Matrix Library +The Nintendo DS boot ROM headers expose a compact matrix helper API in `IrisMTX.h`, which is catalogued in this site's Platinum leak coverage [^6]. +Before looking at the declarations, a few details stand out: +* **Three distinct matrix shapes** - `Mtx`, `Mtx33`, and `Mtx44` map neatly onto affine transforms, rotation/scale-only transforms, and full projection-oriented matrices +* **A compact affine default** - `Mtx` is stored as `4x3`, which is a strong hint that the DS SDK treated full `4x4` projection as a special case rather than the default +* **Order-aware helpers** - Functions such as `TranslateBefore`, `TranslateAfter`, `ScaleBefore`, and `ScaleAfter` make transform order explicit +* **Quantized angle domains** - Rotation helpers with `256`, `1024`, and `4096` variants suggest lookup-table or fixed-point style angle representations rather than plain floating-point radians +* **Precomputed trig and fast paths** - `SinCos` and `Fast` variants show the same performance-conscious flavour we already saw in the DS vector library +* **Full camera pipeline support** - `LookAt`, `Perspective`, `Frustum`, and `Ortho` show that the SDK was trying to cover the whole transform stack, not just local object movement + +The split between `Mtx33`, `Mtx`, and `Mtx44` is especially informative. +A `3x3` matrix is enough for orientation and scale, a `4x3` matrix is enough for most model or view transforms, and the header explicitly reserves `4x4` matrices for cases such as projection. +That is a very game-engine-shaped design. + +The `256`, `1024`, and `4096` rotation suffixes are also a good clue that the SDK expected developers to work with quantized turn units rather than only with conventional floating-point radians. +Those variants likely correspond to different angle-resolution domains or lookup-table granularities. {% capture matrix_types_tab %} -Here are the types it provides to the developer: +Here are the main storage types exposed by the header: ```c typedef s32 MtxRow3_t[3]; @@ -67,33 +284,34 @@ typedef MtxRow4 Mtx44Row; typedef vl MtxRow4 vMtxRow4; typedef vl Mtx44Row vMtx44Row; +// compact 4x3 affine matrix typedef s32 Mtx_t[4][3]; typedef union { Mtx_t m; } Mtx; -typedef vl Mtx vMtx; +typedef vl Mtx vMtx; +// 3x3 rotation/scale matrix typedef s32 Mtx33_t[3][3]; typedef union { Mtx33_t m; } Mtx33; -typedef vl Mtx33 vMtx33; +typedef vl Mtx33 vMtx33; -// used for projection matrix +// full 4x4 matrix, used for projection typedef s32 Mtx44_t[4][4]; typedef union { Mtx44_t m; } Mtx44; -typedef vl Mtx44 vMtx44; +typedef vl Mtx44 vMtx44; ``` {% endcapture %} {% capture matrix_functions_tab %} -Here are a all of the functions it provides: - -```c +Here are the main matrix helpers exposed by the header: +```c void MTX_Identity(Mtx *dstp); void MTX33_Identity(Mtx33 *dstp); void MTX44_Identity(Mtx44 *dstp); @@ -114,9 +332,9 @@ void MTX_Concat(Mtx *src0p, Mtx *src1p, Mtx *dstp); void MTX33_Concat(Mtx33 *src0p, Mtx33 *src1p, Mtx33 *dstp); void MTX44_Concat(Mtx44 *src0p, Mtx44 *src1p, Mtx44 *dstp); -void MTX_Transpose( Mtx *srcp, Mtx *dstp); -void MTX33_Transpose( Mtx33 *srcp, Mtx33 *dstp); -void MTX44_Transpose( Mtx44 *srcp, Mtx44 *dstp); +void MTX_Transpose(Mtx *srcp, Mtx *dstp); +void MTX33_Transpose(Mtx33 *srcp, Mtx33 *dstp); +void MTX44_Transpose(Mtx44 *srcp, Mtx44 *dstp); void MTX_Translate(Mtx *dstp, s32 x, s32 y, s32 z); void MTX44_Translate(Mtx44 *dstp, s32 x, s32 y, s32 z); @@ -139,17 +357,17 @@ void MTX_ScaleAfter(Mtx *srcp, Mtx *dstp, s32 xS, s32 yS, s32 zS); void MTX33_ScaleAfter(Mtx33 *srcp, Mtx33 *dstp, s32 xS, s32 yS, s32 zS); void MTX44_ScaleAfter(Mtx44 *srcp, Mtx44 *dstp, s32 xS, s32 yS, s32 zS); -#define MTX_RotateX(dstp, theta) MTXxx_RotatePriv(_, X, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX_RotateY(dstp, theta) MTXxx_RotatePriv(_, Y, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX_RotateZ(dstp, theta) MTXxx_RotatePriv(_, Z, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX_RotateX(dstp, theta) MTXxx_RotatePriv(_, X, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX_RotateY(dstp, theta) MTXxx_RotatePriv(_, Y, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX_RotateZ(dstp, theta) MTXxx_RotatePriv(_, Z, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX33_RotateX(dstp, theta) MTXxx_RotatePriv(33_, X, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX33_RotateY(dstp, theta) MTXxx_RotatePriv(33_, Y, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX33_RotateZ(dstp, theta) MTXxx_RotatePriv(33_, Z, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX33_RotateX(dstp, theta) MTXxx_RotatePriv(33_, X, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX33_RotateY(dstp, theta) MTXxx_RotatePriv(33_, Y, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX33_RotateZ(dstp, theta) MTXxx_RotatePriv(33_, Z, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX44_RotateX(dstp, theta) MTXxx_RotatePriv(44_, X, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX44_RotateY(dstp, theta) MTXxx_RotatePriv(44_, Y, SIN_NDIV_DEFAULT, dstp, theta) -#define MTX44_RotateZ(dstp, theta) MTXxx_RotatePriv(44_, Z, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX44_RotateX(dstp, theta) MTXxx_RotatePriv(44_, X, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX44_RotateY(dstp, theta) MTXxx_RotatePriv(44_, Y, SIN_NDIV_DEFAULT, dstp, theta) +#define MTX44_RotateZ(dstp, theta) MTXxx_RotatePriv(44_, Z, SIN_NDIV_DEFAULT, dstp, theta) void MTX_RotateX256(Mtx *dstp, u32 theta); void MTX_RotateY256(Mtx *dstp, u32 theta); @@ -181,8 +399,8 @@ void MTX44_RotateX4096(Mtx44 *dstp, u32 theta); void MTX44_RotateY4096(Mtx44 *dstp, u32 theta); void MTX44_RotateZ4096(Mtx44 *dstp, u32 theta); -#define MTXxx_RotatePriv(xx_, axis, ndiv, dstp, theta) MTXxx_RotateNDiv(xx_, axis, ndiv, dstp, theta) -#define MTXxx_RotateNDiv(xx_, axis, ndiv, dstp, theta) MTX##xx_##Rotate##axis##ndiv( dstp, theta) +#define MTXxx_RotatePriv(xx_, axis, ndiv, dstp, theta) MTXxx_RotateNDiv(xx_, axis, ndiv, dstp, theta) +#define MTXxx_RotateNDiv(xx_, axis, ndiv, dstp, theta) MTX##xx_##Rotate##axis##ndiv(dstp, theta) void MTX_RotateXSinCos(Mtx *dstp, s32 sinA, s32 cosA); void MTX_RotateYSinCos(Mtx *dstp, s32 sinA, s32 cosA); @@ -196,14 +414,14 @@ void MTX44_RotateXSinCos(Mtx44 *dstp, s32 sinA, s32 cosA); void MTX44_RotateYSinCos(Mtx44 *dstp, s32 sinA, s32 cosA); void MTX44_RotateZSinCos(Mtx44 *dstp, s32 sinA, s32 cosA); -#define MTX_RotateAxis( dstp, axisp, heta) MTXxx_RotateAxisPriv( _, SIN_NDIV_DEFAULT, dstp, axisp, theta) -#define MTX_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX_RotateAxis(dstp, axisp, theta) MTXxx_RotateAxisPriv(_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(_, SIN_NDIV_DEFAULT, dstp, axisp, theta) -#define MTX33_RotateAxis( dstp, axisp, heta) MTXxx_RotateAxisPriv( 33_, SIN_NDIV_DEFAULT, dstp, axisp, theta) -#define MTX33_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(33_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX33_RotateAxis(dstp, axisp, theta) MTXxx_RotateAxisPriv(33_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX33_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(33_, SIN_NDIV_DEFAULT, dstp, axisp, theta) -#define MTX44_RotateAxis( dstp, axisp, heta) MTXxx_RotateAxisPriv( 44_, SIN_NDIV_DEFAULT, dstp, axisp, theta) -#define MTX44_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(44_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX44_RotateAxis(dstp, axisp, theta) MTXxx_RotateAxisPriv(44_, SIN_NDIV_DEFAULT, dstp, axisp, theta) +#define MTX44_RotateAxisFast(dstp, axisp, theta) MTXxx_RotateAxisPrivFast(44_, SIN_NDIV_DEFAULT, dstp, axisp, theta) void MTX_RotateAxis256(Mtx *dstp, const Vec *axisp, u32 theta); void MTX_RotateAxis256Fast(Mtx *dstp, const Vec *axisp, u32 theta); @@ -226,11 +444,10 @@ void MTX44_RotateAxis1024Fast(Mtx44 *dstp, const Vec *axisp, u32 theta); void MTX44_RotateAxis4096(Mtx44 *dstp, const Vec *axisp, u32 theta); void MTX44_RotateAxis4096Fast(Mtx44 *dstp, const Vec *axisp, u32 theta); -#define MTXxx_RotateAxisPriv( xx_, ndiv, dstp, axisp, theta) MTXxx_RotateAxisNDiv( xx_, ndiv, dstp, axisp, theta) +#define MTXxx_RotateAxisPriv(xx_, ndiv, dstp, axisp, theta) MTXxx_RotateAxisNDiv(xx_, ndiv, dstp, axisp, theta) #define MTXxx_RotateAxisPrivFast(xx_, ndiv, dstp, axisp, theta) MTXxx_RotateAxisNDivFast(xx_, ndiv, dstp, axisp, theta) -#define MTXxx_RotateAxisNDiv( xx_, ndiv, dstp, axisp, theta) MTX##xx_##RotateAxis##ndiv( dstp, axisp, theta) -#define MTXxx_RotateAxisNDivFast(xx_, ndiv, dstp, axisp, theta) MTX##xx_##RotateAxis##ndiv##Fast( dstp, axisp, theta) - +#define MTXxx_RotateAxisNDiv(xx_, ndiv, dstp, axisp, theta) MTX##xx_##RotateAxis##ndiv(dstp, axisp, theta) +#define MTXxx_RotateAxisNDivFast(xx_, ndiv, dstp, axisp, theta) MTX##xx_##RotateAxis##ndiv##Fast(dstp, axisp, theta) void MTX_RotateAxisSinCos(Mtx *dstp, const Vec *axisp, s32 sinA, s32 cosA); void MTX_RotateAxisSinCosFast(Mtx *dstp, const Vec *axisp, s32 sinA, s32 cosA); @@ -254,36 +471,35 @@ void MTX33_QuatMtxFast(Mtx33 *dstp, const Quat *quatp); void MTX44_QuatMtx(Mtx44 *dstp, const Quat *quatp); void MTX44_QuatMtxFast(Mtx44 *dstp, const Quat *quatp); -void MTX_LookAt( Mtx *dstp, const Pos *eye, const Pos *at, const Vec *vUp, Vec *vDst); +void MTX_LookAt(Mtx *dstp, const Pos *eye, const Pos *at, const Vec *vUp, Vec *vDst); void MTX_LookAtFast(Mtx *dstp, const Pos *eye, const Pos *at, const Vec *vUp, Vec *vDst); -#define MTX44_Perspective(dstp, fovy, aspect, near, far, scaleW) MTX44_PerspectivePriv(SIN_NDIV_DEFAULT, dstp, fovy, aspect, near, far, scaleW) +#define MTX44_Perspective(dstp, fovy, aspect, near, far, scaleW) MTX44_PerspectivePriv(SIN_NDIV_DEFAULT, dstp, fovy, aspect, near, far, scaleW) void MTX44_Perspective256(Mtx44 *dstp, u32 fovy, s32 aspect, s32 near, s32 far, s32 scaleW); void MTX44_Perspective1024(Mtx44 *dstp, u32 fovy, s32 aspect, s32 near, s32 far, s32 scaleW); void MTX44_Perspective4096(Mtx44 *dstp, u32 fovy, s32 aspect, s32 near, s32 far, s32 scaleW); -#define MTX44_PerspectivePriv(ndiv, dstp, fovy, aspect, near, far, scaleW) MTX44_PerspectiveNDiv(ndiv, dstp, fovy, aspect, near, far, scaleW) -#define MTX44_PerspectiveNDiv(ndiv, dstp, fovy, aspect, near, far, scaleW) MTX44_Perspective##ndiv( dstp, fovy, aspect, near, far, scaleW) +#define MTX44_PerspectivePriv(ndiv, dstp, fovy, aspect, near, far, scaleW) MTX44_PerspectiveNDiv(ndiv, dstp, fovy, aspect, near, far, scaleW) +#define MTX44_PerspectiveNDiv(ndiv, dstp, fovy, aspect, near, far, scaleW) MTX44_Perspective##ndiv(dstp, fovy, aspect, near, far, scaleW) void MTX44_PerspectiveSinCos(Mtx44 *dstp, s32 sinA, s32 aspect, s32 near, s32 far, s32 scaleW, s32 cosA); void MTX44_Frustum(Mtx44 *dstp, s32 t, s32 b, s32 l, s32 r, s32 n, s32 f, s32 scaleW); +void MTX44_Ortho(Mtx44 *dstp, s32 t, s32 b, s32 l, s32 r, s32 n, s32 f, s32 scaleW); -void MTX44_Ortho( Mtx44 *dstp, s32 t, s32 b, s32 l, s32 r, s32 n, s32 f, s32 scaleW); - -void MTX_MultVec( const Mtx *mult, Vec *srcp, Vec *dstp); +void MTX_MultVec(const Mtx *mult, Vec *srcp, Vec *dstp); void MTX33_MultVec(const Mtx33 *mult, Vec *srcp, Vec *dstp); void MTX44_MultVec(const Mtx44 *mult, Vec *srcp, Vec *dstp); -void MTX_MultVecArray( const Mtx *mult, Vec *srcBasep, Vec *dstBasep, u32 count); +void MTX_MultVecArray(const Mtx *mult, Vec *srcBasep, Vec *dstBasep, u32 count); void MTX33_MultVecArray(const Mtx33 *mult, Vec *srcBasep, Vec *dstBasep, u32 count); void MTX44_MultVecArray(const Mtx44 *mult, Vec *srcBasep, Vec *dstBasep, u32 count); -void MTX_MultVecSR( const Mtx *mult, Vec *srcp, Vec *dstp); +void MTX_MultVecSR(const Mtx *mult, Vec *srcp, Vec *dstp); void MTX44_MultVecSR(const Mtx44 *mult, Vec *srcp, Vec *dstp); -void MTX_MultVecArraySR( const Mtx *mult, Vec *srcBasep, Vec *dstBasep, u32 count); +void MTX_MultVecArraySR(const Mtx *mult, Vec *srcBasep, Vec *dstBasep, u32 count); void MTX44_MultVecArraySR(const Mtx44 *mult, Vec *srcBasep, Vec *dstBasep, u32 count); ``` @@ -291,6 +507,161 @@ void MTX44_MultVecArraySR(const Mtx44 *mult, Vec *srcBasep, Vec *dstBasep, u32 c {% capture matrix_tabs %} {% include rr-tab.html title="DS Matrix Types" default=true content=matrix_types_tab %} -{% include rr-tab.html title="DS Martix Functions" content=matrix_functions_tab %} +{% include rr-tab.html title="DS Matrix Functions" content=matrix_functions_tab %} {% endcapture %} -{% include rr-tabs.html group="group1" tabs=matrix_tabs %} +{% include rr-tabs.html group="ds-matrix-group" tabs=matrix_tabs %} + +Names like `MTX_MultVecSR` are also revealing. +That suffix likely means "scale and rotation only", which is exactly the distinction you want when transforming directions or normals without accidentally applying translation. + +You can find out more about the Nintendo DS boot ROM in the Platinum leak: +{% include_cached link-to-other-post.html post="/platinumleak" description="For more information on the Nintendo Platinum leak that exposed these DS headers, check out this post." %} + +--- +### Sony PSP Matrix Library +The official PlayStation Portable (PSP) SDK exposes matrix types through `psptypes.h` and matrix helpers through the VFPU library header `libvfpu.h` [^7]. +Compared with the DS headers, this API feels much closer to a modern graphics math layer built directly around floating-point transforms and rendering workloads. + +Several details stand out immediately: +* **Float-first matrix types** - The core storage types are `ScePspFMatrix2`, `ScePspFMatrix3`, and `ScePspFMatrix4`, which matches the PSP VFPU's vector-oriented floating-point design +* **16-byte alignment on 4D data** - `ScePspFMatrix4` and related `Vector4` types are aligned for VFPU-friendly access and bulk operations +* **Matrix storage as vector lanes** - The `x`, `y`, `z`, and `w` fields are themselves vectors, and the unions let the same data be viewed as vectors, raw `float[4][4]` storage, or 128-bit quads +* **Translation as a dedicated lane** - The implementation of `sceVfpuMatrix4SetTransfer` and `sceVfpuMatrix4GetTransfer` shows that the `w` vector is used as the translation lane in the 4D matrix form [^8] +* **A full camera pipeline** - The SDK does not stop at identity, multiplication, and transpose. It also includes `LookAt`, perspective, orthographic, view-screen, drop-shadow, and combined transform-plus-perspective helpers + +The PSP matrix API is also more explicit about the jump from algebra to rendering. +Functions such as `sceVfpuLookAtMatrix`, `sceVfpuPerspectiveMatrix`, `sceVfpuViewScreenMatrix`, and `sceVfpuRotTransPers` show that the SDK was designed to help developers move directly from world-space transforms to projected screen-space results [^8]. +As with the PSP vector library, `XYZ` variants usually mean "operate on the spatial part only", so helpers such as `ApplyXYZ` or `NormalizeXYZ` treat `x`, `y`, and `z` as the active transform data while preserving or sidelining the `w` component as needed [^8]. + +{% capture psp_matrix_types_tab %} +Here are the main storage types exposed by `psptypes.h`. +Like the PSP vector types, these matrices are built from named vector lanes and then wrapped in unions so the same memory can be viewed in several ways: + +```c +// 2D matrices +typedef struct ScePspFMatrix2 { + ScePspFVector2 x, y; +} ScePspFMatrix2; + +typedef union ScePspMatrix2 { + ScePspFMatrix2 fm; + ScePspFVector2 fv[2]; + float f[2][2]; + SceULong128 qw[2]; +} ScePspMatrix2; + +// 3D matrices +typedef struct ScePspFMatrix3 { + ScePspFVector3 x, y, z; +} ScePspFMatrix3; + +typedef union ScePspMatrix3 { + ScePspFMatrix3 fm; + ScePspFVector3 fv[3]; + float f[3][3]; + SceULong128 qw[3]; +} ScePspMatrix3; + +// 4D matrices +typedef struct ScePspFMatrix4 { + ScePspFVector4 x, y, z, w; +} ScePspFMatrix4 __attribute__((aligned(16))); + +typedef struct ScePspFMatrix4Unaligned { + ScePspFVector4Unaligned x, y, z, w; +} ScePspFMatrix4Unaligned; + +typedef union ScePspMatrix4 { + ScePspFMatrix4 fm; + ScePspFVector4 fv[4]; + float f[4][4]; + SceULong128 qw[4]; +} ScePspMatrix4 __attribute__((aligned(16))); +``` +{% endcapture %} + +{% capture psp_matrix_functions_tab %} +Here are the main matrix helpers exposed by `libvfpu.h`. +As with the PSP vector section, repeated `2`/`3`/`4` families are compacted into one representative declaration with related variants in a trailing comment: + +```c +// Identity, zero, and copy +#define sceVfpuMatrix2Identity(_pm) sceVfpuMatrix2Unit(_pm) // Variants: sceVfpuMatrix3Identity, sceVfpuMatrix4Identity +ScePspFMatrix2 *sceVfpuMatrix2Unit(ScePspFMatrix2 *pm); // Variants: sceVfpuMatrix3Unit, sceVfpuMatrix4Unit +#define sceVfpuMatrix2Null(_pm) sceVfpuMatrix2Zero(_pm) // Variants: sceVfpuMatrix3Null, sceVfpuMatrix4Null +ScePspFMatrix2 *sceVfpuMatrix2Zero(ScePspFMatrix2 *pm); // Variants: sceVfpuMatrix3Zero, sceVfpuMatrix4Zero +ScePspFMatrix2 *sceVfpuMatrix2Copy(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1); // Variants: sceVfpuMatrix3Copy, sceVfpuMatrix4Copy + +// Apply matrices to vectors +ScePspFVector2 *sceVfpuMatrix2Apply(ScePspFVector2 *pv0, const ScePspFMatrix2 *pm0, const ScePspFVector2 *pv1); // Variants: sceVfpuMatrix3Apply, sceVfpuMatrix4Apply +ScePspFVector4 *sceVfpuMatrix4ApplyXYZ(ScePspFVector4 *pv0, const ScePspFMatrix4 *pm0, const ScePspFVector4 *pv1); + +// Matrix-matrix operations +ScePspFMatrix2 *sceVfpuMatrix2Mul(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1, const ScePspFMatrix2 *pm2); // Variants: sceVfpuMatrix3Mul, sceVfpuMatrix4Mul +ScePspFMatrix2 *sceVfpuMatrix2Scale(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1, float s); // Variants: sceVfpuMatrix3Scale, sceVfpuMatrix4Scale +ScePspFMatrix2 *sceVfpuMatrix2Transpose(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1); // Variants: sceVfpuMatrix3Transpose, sceVfpuMatrix4Transpose + +// Normalization and inverse +#define sceVfpuMatrix4Invers(_m0,_m1) sceVfpuMatrix4Inverse(_m0,_m1) +ScePspFMatrix4 *sceVfpuMatrix4Inverse(ScePspFMatrix4 *pm0, const ScePspFMatrix4 *pm1); +ScePspFMatrix3 *sceVfpuMatrix3Normalize(ScePspFMatrix3 *pm0, const ScePspFMatrix3 *pm1); +ScePspFMatrix4 *sceVfpuMatrix4NormalizeXYZ(ScePspFMatrix4 *pm0, const ScePspFMatrix4 *pm1); + +// Rotation builders +ScePspFMatrix2 *sceVfpuMatrix2RotZ(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1, float rz); +ScePspFMatrix3 *sceVfpuMatrix3RotX(ScePspFMatrix3 *pm0, const ScePspFMatrix3 *pm1, float rx); // Variants: sceVfpuMatrix3RotY, sceVfpuMatrix3RotZ +ScePspFMatrix3 *sceVfpuMatrix3Rot(ScePspFMatrix3 *pm0, const ScePspFMatrix3 *pm1, const ScePspFVector3 *rot); +ScePspFMatrix4 *sceVfpuMatrix4RotX(ScePspFMatrix4 *pm0, const ScePspFMatrix4 *pm1, float rx); // Variants: sceVfpuMatrix4RotY, sceVfpuMatrix4RotZ +ScePspFMatrix4 *sceVfpuMatrix4Rot(ScePspFMatrix4 *pm0, const ScePspFMatrix4 *pm1, const ScePspFVector4 *rot); + +// Translation lane helpers +ScePspFMatrix4 *sceVfpuMatrix4Transfer(ScePspFMatrix4 *pm0, const ScePspFMatrix4 *pm1, const ScePspFVector4 *ptv); +ScePspFMatrix4 *sceVfpuMatrix4SetTransfer(ScePspFMatrix4 *pm, const ScePspFVector4 *ptv); +ScePspFVector4 *sceVfpuMatrix4GetTransfer(ScePspFVector4 *pv, const ScePspFMatrix4 *pm); + +// Queries and algebra helpers +#define sceVfpuMatrix2IsIdentity(_pm) sceVfpuMatrix2IsUnit(_pm) // Variants: sceVfpuMatrix3IsIdentity, sceVfpuMatrix4IsIdentity +SceBool sceVfpuMatrix2IsUnit(const ScePspFMatrix2 *pm); // Variants: sceVfpuMatrix3IsUnit, sceVfpuMatrix4IsUnit +float sceVfpuMatrix2Trace(const ScePspFMatrix2 *pm); // Variants: sceVfpuMatrix3Trace, sceVfpuMatrix4Trace +float sceVfpuMatrix2Determinant(const ScePspFMatrix2 *pm); // Variants: sceVfpuMatrix3Determinant, sceVfpuMatrix4Determinant +ScePspFMatrix2 *sceVfpuMatrix2Adjoint(ScePspFMatrix2 *pm0, const ScePspFMatrix2 *pm1); // Variants: sceVfpuMatrix3Adjoint, sceVfpuMatrix4Adjoint + +// Matrix and quaternion bridge +ScePspFMatrix4 *sceVfpuQuaternionToMatrix(ScePspFMatrix4 *pm, const ScePspFQuaternion *pq); +ScePspFQuaternion *sceVfpuQuaternionFromMatrix(ScePspFQuaternion *pq, const ScePspFMatrix4 *pm); + +// Camera, projection, and render-pipeline helpers +ScePspFMatrix4 *sceVfpuLookAtMatrix(ScePspFMatrix4 *pm0, const ScePspFVector4 *pvEye, const ScePspFVector4 *pvCenter, const ScePspFVector4 *pvUp); +ScePspFMatrix4 *sceVfpuPerspectiveMatrix(ScePspFMatrix4 *pm0, float fovy, float aspect, float r_near, float r_far); +ScePspFMatrix4 *sceVfpuOrthoMatrix(ScePspFMatrix4 *pm0, float left, float right, float bottom, float top, float r_near, float r_far); +ScePspFMatrix4 *sceVfpuCameraMatrix(ScePspFMatrix4 *pm, const ScePspFVector4 *p, const ScePspFVector4 *zd, const ScePspFVector4 *yd); +ScePspFMatrix4 *sceVfpuViewScreenMatrix(ScePspFMatrix4 *pm, float scrz, float ax, float ay, float cx, float cy, float zmin, float zmax, float nearz, float farz); +ScePspFMatrix4 *sceVfpuDropShadowMatrix(ScePspFMatrix4 *pm, const ScePspFVector4 *lp, float a, float b, float c, int mode); +ScePspFVector4 *sceVfpuRotTransPers(ScePspFVector4 *pv0, const ScePspFMatrix4 *pm0, const ScePspFVector4 *pv1); +int sceVfpuRotTransPersN(short *pXyz, int pitch, const ScePspFMatrix4 *pm0, const ScePspFVector4 *pv1, int n); +``` +{% endcapture %} + +{% capture psp_matrix_tabs %} +{% include rr-tab.html title="PSP Matrix Types" default=true content=psp_matrix_types_tab %} +{% include rr-tab.html title="PSP Matrix Functions" content=psp_matrix_functions_tab %} +{% endcapture %} +{% include rr-tabs.html group="psp-matrix-group" tabs=psp_matrix_tabs %} + +The PSP SDK function names also reveal how the library is meant to be used in practice [^8]: +* `Apply` and `Mul` map directly onto VFPU matrix-transform instructions in the source, which makes the whole API feel like a thin but useful abstraction over the hardware +* `Transfer`, `SetTransfer`, and `GetTransfer` are the PSP SDK's names for translation helpers rather than more modern names like `Translate` or `SetTranslation` +* `LookAtMatrix` builds a camera basis from eye, target, and up vectors using cross products and normalization +* `PerspectiveMatrix`, `OrthoMatrix`, and `ViewScreenMatrix` show the full path from camera space to projected and screen-oriented coordinates +* `RotTransPers` and `RotTransPersN` are especially game-oriented because they combine transform and perspective divide in one step, with the `N` variant handling batches of points + +--- +# References +[^1]: [Unity Scripting API - Matrix4x4](https://docs.unity3d.com/ScriptReference/Matrix4x4.html) +[^2]: [Unity Scripting API - Matrix4x4.TRS](https://docs.unity3d.com/ScriptReference/Matrix4x4.TRS.html) +[^3]: [Unity Scripting API - Matrix4x4.MultiplyPoint3x4](https://docs.unity3d.com/ScriptReference/Matrix4x4.MultiplyPoint3x4.html) +[^4]: [Unity Scripting API - Matrix4x4.transpose](https://docs.unity3d.com/ScriptReference/Matrix4x4-transpose.html) +[^5]: [Unity Scripting API - Matrix4x4.Perspective](https://docs.unity3d.com/ScriptReference/Matrix4x4.Perspective.html) +[^6]: [RetroReversing - Nintendo Platinum Leak](/platinumleak) +[^7]: Sony PSP SDK headers `psptypes.h` and `libvfpu.h`. +[^8]: Sony PSP SDK implementations `src/vfpu/matrix2.c`, `src/vfpu/matrix3.c`, `src/vfpu/matrix4.c`, and `src/vfpu/perspective.c`. diff --git a/pages/general/maths/Quaternions.md b/pages/general/maths/Quaternions.md index bed70679..7f852985 100644 --- a/pages/general/maths/Quaternions.md +++ b/pages/general/maths/Quaternions.md @@ -1,22 +1,29 @@ --- layout: post -tags: +tags: - maths +- psp +- ds title: Quaternions (Maths for Game Developers) category: maths permalink: /Quaternions breadcrumbs: - name: Home url: / + - name: Introduction + url: /introduction - name: Maths for Game Developers url: /maths - name: Quaternions url: # -recommend: - - maths - - introduction - - sdk -editlink: /articles/maths/Quaternions.md +recommend: +- maths +- introduction +- sdk +- psp +- ds +editlink: /pages/general/maths/Quaternions.md +updatedAt: '2026-04-26' videocarousel: - title: Part 1 Gimbal Lock image: https://i.ytimg.com/vi/zc8b2Jo7mno/sddefault.jpg @@ -26,35 +33,232 @@ videocarousel: youtube: 'OmCzZ-D8Wdk' --- -# Why Quaternions? -If you have used any 3D editor before then you are probably used to seeing 3D rotation in terms of X,Y,Z degrees of rotation, e.g 45 degrees in X, 90 in Y and 25 in Z. These are called Euler angles and they are fairly easy for the user to understand. +# Introduction to Quaternions +A quaternion is a four-number rotation representation, usually written as `{ x, y, z, w }` or as a vector part plus a scalar part [^1]. +In game code, the main reason to care about quaternions is not that they are more "advanced" than Euler angles, but that they store orientation in a way that composes cleanly and interpolates well [^1][^2]. + +Quaternions are useful in games for several common jobs: +* **Orientation storage** - Keep a camera, character, or bone rotation in a compact form +* **Rotation composition** - Combine turns without having to manage three separate Euler channels +* **Interpolation** - Blend smoothly between poses with `Slerp` or related helpers [^3] +* **Matrix generation** - Convert a rotation into a matrix when the renderer or transform stack needs one [^1] + +The most important limitation is that quaternions are mainly for rotation. +They are not a general replacement for vectors or matrices. +Vectors still represent positions and directions, while matrices still matter for full transform pipelines and projection: +{% include_cached link-to-other-post.html post="/Vectors" description="Vectors describe directions, offsets, and magnitudes that quaternions often rotate." %} +{% include_cached link-to-other-post.html post="/Matrix" description="Matrices package rotation, translation, scale, and projection, and engines often convert quaternions into matrices for rendering." %} + +--- +## Glossary of Key Terms +If you are new to rotation-heavy game math, these are the main terms worth keeping straight: +* **Euler angles** - A rotation described as separate turns around named axes such as `X`, `Y`, and `Z`. +* **Axis-angle** - A rotation described as "rotate by this angle around this axis". +* **Unit quaternion** - A quaternion with magnitude `1`, which is the form used to represent valid rotations. +* **SLERP** - Spherical linear interpolation, used to blend smoothly between two unit quaternions [^3]. + +--- +## Core Quaternion Concepts +### Why games use quaternions +The big practical win is that quaternions treat orientation as one object instead of three loosely related angle channels [^1]. +That makes them a better fit for camera systems, skeletal animation, aiming, and smooth turning. + +Compared with storing raw Euler angles, quaternions usually give you: +* **Cleaner interpolation** - Rotations can be blended directly with `Slerp` [^3] +* **Stable composition** - Repeated rotation updates do not require constant angle-order bookkeeping +* **A better internal form** - Engines can still expose Euler angles in tools, while storing quaternions under the hood [^1] + +That does not mean Euler angles disappear completely. +They are still useful for editor UIs, debug displays, and human-readable inputs. +The common pattern is "Euler for the user, quaternion for the engine". + +### Identity and unit quaternions +The identity quaternion is the neutral rotation, usually `{ x: 0, y: 0, z: 0, w: 1 }` [^1]. +Valid rotation quaternions are usually normalized, which means they have magnitude `1` [^1][^4]. + +This is why engines often normalize a quaternion after repeated math operations: + +```ts +type Quat = { x: number; y: number; z: number; w: number }; + +function quaternionMagnitude(q: Quat): number { + return Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); +} + +function normalizeQuaternion(q: Quat): Quat { + const len = quaternionMagnitude(q); + if (len === 0) { + return { x: 0, y: 0, z: 0, w: 1 }; + } + + return { + x: q.x / len, + y: q.y / len, + z: q.z / len, + w: q.w / len, + }; +} + +const driftedTurn = { x: 0, y: 0.8, z: 0, w: 0.8 }; +const repairedTurn = normalizeQuaternion(driftedTurn); +``` + +In this example `repairedTurn` becomes approximately `{ x: 0, y: 0.7071, z: 0, w: 0.7071 }`. +That matters because a quaternion that drifts away from unit length stops behaving like a clean pure rotation. +Returning identity in the zero-length case is just a defensive code choice for the example. + +### Axis-angle conversion +One of the easiest ways to think about a quaternion is through axis-angle input. +Many SDKs build a quaternion from "rotate by this amount around this axis", because that is much easier to reason about than editing raw `x`, `y`, `z`, and `w` by hand [^2]. + +This small helper converts an axis-angle turn into a quaternion: + +```ts +type Vec3 = { x: number; y: number; z: number }; +type Quat = { x: number; y: number; z: number; w: number }; + +function fromAxisAngle(axis: Vec3, radians: number): Quat { + const axisLen = Math.sqrt(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z); + const unitAxis = { + x: axis.x / axisLen, + y: axis.y / axisLen, + z: axis.z / axisLen, + }; -So you would think that when programming games you would just use these Euler angles to rotate your 3d models. But sadly these Euler angles have a big flaw called **Gimbal Lock**, the solution is to instead use Quaternions to store the rotation information of your 3D model. + const half = radians * 0.5; + const sinHalf = Math.sin(half); + return { + x: unitAxis.x * sinHalf, + y: unitAxis.y * sinHalf, + z: unitAxis.z * sinHalf, + w: Math.cos(half), + }; +} + +const quarterTurnRight = fromAxisAngle({ x: 0, y: 1, z: 0 }, Math.PI / 2); +``` + +In this example `quarterTurnRight` becomes approximately `{ x: 0, y: 0.7071, z: 0, w: 0.7071 }`. +That is the same idea exposed later by the DS `VEC_Conv2Quat*` helpers and the PSP `sceVfpuQuaternionFromRotate` function. + +### Quaternion multiplication and order +Quaternion multiplication combines rotations [^1]. +This is one of the most useful operations on the page, but it also carries one of the most important warnings: rotation order matters. +Quaternion multiplication is not commutative, so `a * b` and `b * a` usually mean different things [^1]. + +This example uses one yaw turn and one pitch turn: + +```ts +type Quat = { x: number; y: number; z: number; w: number }; + +function multiply(a: Quat, b: Quat): Quat { + return { + x: a.w * b.x + b.w * a.x + a.y * b.z - a.z * b.y, + y: a.w * b.y + b.w * a.y + a.z * b.x - a.x * b.z, + z: a.w * b.z + b.w * a.z + a.x * b.y - a.y * b.x, + w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z, + }; +} + +const yaw90 = { x: 0, y: 0.7071, z: 0, w: 0.7071 }; +const pitch45 = { x: 0.3827, y: 0, z: 0, w: 0.9239 }; + +const yawThenPitch = multiply(pitch45, yaw90); +const pitchThenYaw = multiply(yaw90, pitch45); +``` + +Here `yawThenPitch` becomes approximately `{ x: 0.2706, y: 0.6533, z: 0.2706, w: 0.6533 }`, while `pitchThenYaw` becomes approximately `{ x: 0.2706, y: 0.6533, z: -0.2706, w: 0.6533 }`. +That sign flip in `z` is a reminder that changing order changes the resulting orientation. +Exact interpretation depends on the engine's multiplication convention, but the non-commutative nature is universal. + +### SLERP +SLERP is the standard high-quality way to interpolate between two unit quaternions [^3]. +It is especially useful for camera turns, animation blending, and "turn smoothly toward target" behaviours because it follows the surface of the unit sphere instead of blending components in a straight line [^3]. + +This is the kind of gameplay-facing usage most engines expose: + +```ts +const identity = { x: 0, y: 0, z: 0, w: 1 }; +const ninetyDegreeYaw = { x: 0, y: 0.7071, z: 0, w: 0.7071 }; + +// A real engine helper would perform quaternion SLERP here. +// At t = 0.5 the result is half way around the unit sphere. +const halfTurn = { x: 0, y: 0.3827, z: 0, w: 0.9239 }; +``` + +In this example `halfTurn` is the quaternion for a `45` degree yaw, which is why SLERP is so common in smooth-turning systems. +Many SDKs also expose cheaper `Lerp` variants beside `Slerp`, which usually signals a quality-versus-speed tradeoff. + +--- ## Gimbal Lock -Gimbal lock happens when 2 of the plains align on the same axis and results in weird animation in 3D space. - +Gimbal lock is one of the main reasons engines avoid storing long-lived 3D orientation purely as Euler angles. +It happens when two rotation axes align, reducing the effective degrees of freedom and making some turns ambiguous or awkward to control. +This is not "a quaternion problem". +It is a problem with representing orientation as stacked axis rotations. + +[GuerrillaCG](https://www.youtube.com/watch?v=zc8b2Jo7mno) gives a clear visual explanation of how Euler rotations can lose a degree of freedom when axes line up, and why that becomes a practical issue for animation and camera systems. + + + +Quaternions help because the engine can store and compose orientation without constantly re-entering the Euler-angle representation. +That does not mean every quaternion-based workflow is magically free of bad controls. +If a tool keeps converting back to Euler angles for editing, or if the game logic still thinks in Euler order all the time, some of the same practical confusion can reappear. ### Gimbal Lock and Apollo 13 -If you think Gimbal lock only affects game developers then try being an aerospace engineer! - +The engineering consequences are easier to remember when you see a real-world case. +[The Vintage Space](https://www.youtube.com/watch?v=OmCzZ-D8Wdk) explains how gimbal lock affected Apollo-era spacecraft navigation and why alignment problems in rotational systems mattered far beyond games. + + --- -# Writing your own Quaternion Library -This is an excellent video which covers how to write your own Quaternion library, it uses Java but the concepts can apply to any programming language. - +## Writing your own Quaternion Library +If you want to implement the math yourself, the most useful starting point is usually: +* build quaternions from axis-angle input +* normalize after operations that may accumulate drift +* support multiply, inverse, and dot product +* add `Slerp` once the core representation is behaving correctly + +[thebennybox](https://www.youtube.com/watch?v=GnKGZYcsJ3E) walks through writing a quaternion library in Java, but the real value is the step-by-step explanation of the math and the operations you would mirror in any language. + + --- -# Unity - +## Unity +Unity is a useful modern reference point because it exposes the same operations that show up in older SDKs: identity, angle-axis creation, multiplication, inverse, normalization, and `Slerp` [^1][^2][^3][^4][^5]. +It also makes the "tool UI versus engine storage" split very visible, because you often edit rotations in Euler form in the Inspector while `Transform.rotation` is stored as a quaternion internally [^1]. + +The most common Unity quaternion helpers are: +* **`Quaternion.AngleAxis`** - Build a rotation from an axis and angle [^2] +* **`Quaternion.Slerp`** - Blend smoothly between rotations [^3] +* **`Quaternion.Inverse`** - Compute the opposite rotation [^5] +* **`Quaternion.LookRotation`** - Build an orientation from `forward` and `up` vectors [^6] +* **`Quaternion.ToAngleAxis`** - Convert a quaternion back into axis-angle form [^7] + +[Unity](https://www.youtube.com/watch?v=hd1QzLf4ZH8) provides an intermediate-level tutorial that focuses on how these quaternion helpers appear in day-to-day engine scripting rather than only in pure math form. + + --- -# Nintendo DS -The Nintendo DS Operating System has a small Quaternion helper library defined in the header file **IrisQUAT.h**. This file was leaked as part of the September 2020 "Platinum leak" as it is part of the Nintendo DS Boot ROM. +## Quaternion Libraries used in Retail Console Game Development +Looking at retail SDK headers is useful because it shows which quaternion operations game programmers were expected to rely on in production code. + +### Nintendo DS Official Quaternion Library +The Nintendo DS operating system exposes a compact quaternion helper API in `IrisQUAT.h`, which is catalogued through the Platinum leak material already linked on this site [^8]. +Before looking at the declarations, a few design choices stand out: +* **Compact default storage** - `Quat` is just the `Quat16` form, which suggests fixed-point-friendly compactness was the normal path +* **A larger precision option** - `Quat32` exists when more range or precision is needed +* **Axis-angle conversion as a first-class helper** - `VEC_Conv2Quat*` shows that building quaternions from an axis and quantized angle was expected to be common +* **Core algebra only** - `DotProduct`, `Normalize`, `Inverse`, `Multiply`, `Add`, `Sub`, and `Scale` cover the main rotation operations without a lot of extra abstraction +* **Both `Lerp` and `Slerp`** - The SDK exposes both cheap linear blending and higher-quality spherical interpolation, which strongly suggests an explicit speed-versus-quality choice + +This DS API also connects nicely with the matrix page. +Once you have a quaternion, the SDK can later convert it into a matrix for transform use through helpers such as `MTX_QuatMtx`: +{% include_cached link-to-other-post.html post="/Matrix" description="The matrix page shows how DS matrices and quaternion-to-matrix conversion fit into the wider transform pipeline." %} {% capture quaternion_types_tab %} -Here are the types it provides to the developer: - +Here are the quaternion storage types exposed by the DS header: + ```c // 16-bit typedef struct { @@ -63,8 +267,8 @@ typedef struct { s16 z; s16 w; } Quat16, Quat; -typedef vl Quat16 vQuat16; -typedef vl Quat vQuat; +typedef vl Quat16 vQuat16; +typedef vl Quat vQuat; // 32-bit typedef struct { @@ -73,14 +277,13 @@ typedef struct { s32 z; s32 w; } Quat32; -typedef vl Quat32 vQuat32; +typedef vl Quat32 vQuat32; ``` - {% endcapture %} {% capture quaternion_functions_tab %} -Here are all of the functions it provides: - +Here are the main quaternion helpers exposed by the DS header: + ```c #define VEC_Conv2Quat(axisp, theta, dstp) VEC_Conv2QuatPriv(SIN_NDIV_DEFAULT, axisp, theta, dstp) @@ -89,33 +292,128 @@ void VEC_Conv2Quat1024(const Vec *axisp, u32 theta, Quat *dstp); void VEC_Conv2Quat4096(const Vec *axisp, u32 theta, Quat *dstp); #define VEC_Conv2QuatPriv(ndiv, axisp, theta, dstp) VEC_Conv2QuatNDiv(ndiv, axisp, theta, dstp) -#define VEC_Conv2QuatNDiv(ndiv, axisp, theta, dstp) VEC_Conv2Quat##ndiv( axisp, theta, dstp) +#define VEC_Conv2QuatNDiv(ndiv, axisp, theta, dstp) VEC_Conv2Quat##ndiv(axisp, theta, dstp) - -s32 QUAT_DotProduct(const Quat *a, const Quat *b); +s32 QUAT_DotProduct(const Quat *a, const Quat *b); void QUAT_Normalize(Quat *srcp, Quat *dstp); - void QUAT_Inverse(Quat *srcp, Quat *dstp); - void QUAT_Multiply(Quat *a, Quat *b, Quat *axb); - void QUAT_Add(Quat *a, Quat *b, Quat *a_b); - void QUAT_Sub(Quat *a, Quat *b, Quat *a_b); - void QUAT_Scale(Quat *srcp, Quat *dstp, s32 scale); +void QUAT_Lerp(Quat *p, Quat *q, Quat *d, s32 t); +void QUAT_Slerp(Quat *p, Quat *q, Quat *d, s32 t); +``` +{% endcapture %} + +{% capture quaternion_tabs %} +{% include rr-tab.html title="DS Quaternion Types" default=true content=quaternion_types_tab %} +{% include rr-tab.html title="DS Quaternion Functions" content=quaternion_functions_tab %} +{% endcapture %} +{% include rr-tabs.html group="ds-quaternion-group" tabs=quaternion_tabs %} -void QUAT_Lerp( Quat *p, Quat *q, Quat *d, s32 t); +The `256`, `1024`, and `4096` suffixes are especially revealing. +They imply the same kind of quantized angle domains seen elsewhere in the DS math headers, which fits the platform's fixed-point and lookup-table-heavy style. +You can find out more about the Nintendo DS boot ROM in the Platinum leak: +{% include_cached link-to-other-post.html post="/platinumleak" description="For more information on the Nintendo Platinum leak that exposed these DS headers, check out this post." %} -void QUAT_Slerp(Quat *p, Quat *q, Quat *d, s32 t); +--- +### Sony PSP Quaternion Library +The official PlayStation Portable (PSP) SDK exposes quaternion storage through `psptypes.h` and quaternion helpers through the VFPU library header `libvfpu.h` [^9]. +Compared with the DS header, the PSP API feels much closer to a modern float-first graphics math layer. + +Several details stand out immediately: +* **Aligned float storage** - `ScePspFQuaternion` is a four-float type aligned to `16` bytes, which fits the VFPU's preferred data shape [^9] +* **Identity as a named quaternion** - The SDK treats `(0, 0, 0, 1)` as a first-class helper rather than assuming developers will construct it manually [^10] +* **A direct matrix bridge** - The API explicitly converts quaternions to and from `Matrix4` form, which matches how real render pipelines still consume matrix transforms [^9][^10] +* **Interpolation depth beyond `Slerp`** - The presence of `Squad` suggests the library was meant to support smoother multi-key rotation blending as well as simple endpoint interpolation [^9][^10] +* **Rotation-order-aware conversion helpers** - `FromRotZYX`, `FromRotXYZ`, `FromRotYXZ`, `ToRotZYX`, and `ToRotYXZ` show that Euler order still matters when you cross back into angle-based representations [^9][^10] + +The PSP implementation also tells us something useful about how these helpers were meant to behave. +`sceVfpuQuaternionApply` rotates a vector by the quaternion, `sceVfpuQuaternionFromRotate` normalizes the input axis internally before building the result, and the `Slerp` implementation explicitly handles the shortest-path case by flipping sign when needed [^10]. + +{% capture psp_quaternion_types_tab %} +Here are the quaternion storage types exposed by `psptypes.h`: + +```c +typedef struct ScePspFQuaternion { + float x, y, z, w; +} ScePspFQuaternion __attribute__((aligned(16))); + +typedef struct ScePspFQuaternionUnaligned { + float x, y, z, w; +} ScePspFQuaternionUnaligned; ``` +{% endcapture %} + +{% capture psp_quaternion_functions_tab %} +Here are the main quaternion helpers exposed by `libvfpu.h`: +```c +// Identity and copy +#define sceVfpuQuaternionIdentity(_pq) sceVfpuQuaternionUnit(_pq) +ScePspFQuaternion *sceVfpuQuaternionUnit(ScePspFQuaternion *pq); +ScePspFQuaternion *sceVfpuQuaternionCopy(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1); + +// Matrix and vector bridge +ScePspFMatrix4 *sceVfpuQuaternionToMatrix(ScePspFMatrix4 *pm, const ScePspFQuaternion *pq); +ScePspFVector4 *sceVfpuQuaternionApply(ScePspFVector4 *pv0, const ScePspFQuaternion *pq, const ScePspFVector4 *pv1); +ScePspFQuaternion *sceVfpuQuaternionFromMatrix(ScePspFQuaternion *pq, const ScePspFMatrix4 *pm); + +// Core quaternion algebra +ScePspFQuaternion *sceVfpuQuaternionAdd(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2); +ScePspFQuaternion *sceVfpuQuaternionSub(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2); +ScePspFQuaternion *sceVfpuQuaternionMul(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2); +#define sceVfpuQuaternionDot(_v1,_v2) sceVfpuQuaternionInnerProduct(_q1,_q2) +float sceVfpuQuaternionInnerProduct(const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2); +ScePspFQuaternion *sceVfpuQuaternionNormalize(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1); +ScePspFQuaternion *sceVfpuQuaternionConj(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1); +ScePspFQuaternion *sceVfpuQuaternionInverse(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1); + +// Interpolation +ScePspFQuaternion *sceVfpuQuaternionSlerp(ScePspFQuaternion *pq0, const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2, float rate); +ScePspFQuaternion *sceVfpuQuaternionSquad(ScePspFQuaternion *pq0, + const ScePspFQuaternion *pq1, const ScePspFQuaternion *pq2, + const ScePspFQuaternion *pq3, const ScePspFQuaternion *pq4, + float t); + +// Conversion from angle-based forms +ScePspFQuaternion *sceVfpuQuaternionFromRotZYX(ScePspFQuaternion *pq, const ScePspFVector4 *pv); +ScePspFQuaternion *sceVfpuQuaternionFromRotXYZ(ScePspFQuaternion *pq, const ScePspFVector4 *pv); +ScePspFQuaternion *sceVfpuQuaternionFromRotYXZ(ScePspFQuaternion *pq, const ScePspFVector4 *pv); +ScePspFQuaternion *sceVfpuQuaternionFromRotate(ScePspFQuaternion *pq, float angle, const ScePspFVector4 *pvAxis); +ScePspFVector4 *sceVfpuQuaternionToRotZYX(ScePspFVector4 *pv, const ScePspFQuaternion *pq); +ScePspFVector4 *sceVfpuQuaternionToRotYXZ(ScePspFVector4 *pv, const ScePspFQuaternion *pq); +``` {% endcapture %} -{% capture quaternion_tabs %} -{% include rr-tab.html title="DS Types" default=true content=quaternion_types_tab %} -{% include rr-tab.html title="DS Functions" content=quaternion_functions_tab %} +{% capture psp_quaternion_tabs %} +{% include rr-tab.html title="PSP Quaternion Types" default=true content=psp_quaternion_types_tab %} +{% include rr-tab.html title="PSP Quaternion Functions" content=psp_quaternion_functions_tab %} {% endcapture %} -{% include rr-tabs.html group="group1" tabs=quaternion_tabs %} +{% include rr-tabs.html group="psp-quaternion-group" tabs=psp_quaternion_tabs %} + +The function names also give away several practical use cases [^10]: +* **`QuaternionApply`** - Rotate vectors directly, which is useful for camera basis vectors, aiming, and transforming local directions into world directions +* **`QuaternionInnerProduct`** - Another name for quaternion dot product, often used to measure orientation similarity or to support interpolation logic +* **`QuaternionConj` and `QuaternionInverse`** - Expose the common "reverse this rotation" operations explicitly instead of forcing programmers to reconstruct them from scratch +* **`QuaternionFromRotate`** - Builds a quaternion from axis-angle input and normalizes the axis internally, which matches the mental model most developers start from +* **`QuaternionToRotZYX` and `QuaternionToRotYXZ`** - Convert back to Euler-style forms, and the source includes special handling near singular cases when extracting those angles + +The PSP header also makes it obvious that quaternion terminology varies. +Its `InnerProduct` naming is just the formal version of quaternion dot product, the same way the PSP vector library uses "inner product" for vectors too. + +--- +# References +[^1]: [Unity Scripting API - Quaternion](https://docs.unity3d.com/ScriptReference/Quaternion.html) +[^2]: [Unity Scripting API - Quaternion.AngleAxis](https://docs.unity3d.com/ScriptReference/Quaternion.AngleAxis.html) +[^3]: [Unity Scripting API - Quaternion.Slerp](https://docs.unity3d.com/ScriptReference/Quaternion.Slerp.html) +[^4]: [Unity Scripting API - Quaternion.Normalize](https://docs.unity3d.com/ScriptReference/Quaternion.Normalize.html) +[^5]: [Unity Scripting API - Quaternion.Inverse](https://docs.unity3d.com/ScriptReference/Quaternion.Inverse.html) +[^6]: [Unity Scripting API - Quaternion.LookRotation](https://docs.unity3d.com/ScriptReference/Quaternion.LookRotation.html) +[^7]: [Unity Scripting API - Quaternion.ToAngleAxis](https://docs.unity3d.com/ScriptReference/Quaternion.ToAngleAxis.html) +[^8]: [RetroReversing - Nintendo Platinum Leak](/platinumleak) +[^9]: Sony PSP SDK headers `psptypes.h` and `libvfpu.h`. +[^10]: Sony PSP SDK implementation `src/vfpu/quaternion.c`. diff --git a/pages/general/maths/Vectors.md b/pages/general/maths/Vectors.md index eb6caa6c..5b25fdd1 100644 --- a/pages/general/maths/Vectors.md +++ b/pages/general/maths/Vectors.md @@ -339,6 +339,7 @@ What makes the PSP vector API interesting is that it looks much closer to a mode The use of floating-point vector types, 16-byte-aligned 4D vectors, and operations such as dot product, cross product, normalization, lerp, reflection, refraction, and face-forward suggests an API designed around the PSP's VFPU and 3D rendering workloads rather than just basic gameplay math. The repeated `XYZ` variants are especially telling, because they imply that many engine data structures were stored in 4D form while still treating only the first three components as position or direction data. +In that style of API, the `w` component is often used for homogeneous-coordinate math, padding/alignment, or some non-spatial extra value while `x`, `y`, and `z` carry the actual spatial direction or position. {% capture psp_vector_types_tab %} Here are the main storage types exposed by the header (`psptypes.h`). @@ -475,6 +476,7 @@ Some of the less obvious helpers are worth explaining before reading the declara * `InnerProduct` is the PSP SDK's formal name for the dot product, while `OuterProduct` is used as the implementation name behind the SDK's cross-product helpers. * `Funnel` is an unusual name, but the implementation shows that it literally sums the components of the vector. `Average` does the same reduction and then divides by the number of components. * `FaceForward`, `Reflect`, and `Refract` are surface-response helpers that fit naturally with lighting, collision response, and other rendering-style calculations. +* The `sceVfpuVector2FaceForwardXYZ` name shown below looks inconsistent with the surrounding 4D `XYZ` helpers. It is reproduced here as written in the SDK header, but it likely reflects a naming mistake or typo in the original API. * `NormalizePhase` is not vector normalization in the usual magnitude sense. The implementation wraps each component back into the `[-pi, +pi]` range, so it is better understood as angle or phase normalisation. ```c