From: A-Larsen Date: Thu, 15 Oct 2026 01:08:58 +0000 (-0400) Subject: first commit X-Git-Url: https://nyquistdev.net/git/?a=commitdiff_plain;h=6866930697dcfb450087ee58ea1d6d37a7bc6531;p=basic-3d-example.git first commit --- 6866930697dcfb450087ee58ea1d6d37a7bc6531 diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..ac1ab78 --- /dev/null +++ b/README.adoc @@ -0,0 +1,420 @@ += Basic 3D Example + +click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=HEAD"[here] for an interactive example + +A good explaination of how 3D works. + +NOTE: This started off as code from Tsoding, I used chatgpt to change + specific things, and also made some of my own changes. This is a learning + tool! not something I wrote off the top of my head + +== Graphics Pipeline + +---- +vertex + -> rotate cube + -> translate cube (object position) + -> apply camera (inverse transform) + -> project + -> screen +---- + +NOTE: Some parts of these pipeline may be left out if that part of the pipeline + is not needed + +== Basic Projection Formula + +Projection formula for a vector latexmath:[[x,y,z]]: + +[latexmath] +++++ +\[ +\begin{aligned} +x' &= x/z \\ +y' &= y/z +\end{aligned} +\] +++++ + +== Vertex Rotation Formulas +click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/simple-rotation[here] for an interactive example + +Rotate vector formula for x-axis (y-z plane): + +[latexmath] +++++ +\[ +\begin{aligned} +x' &= x \\ +y' &= y\cos\theta - z\sin\theta \\ +z' &= y\sin\theta + z\cos\theta +\end{aligned} +\] +++++ + +Rotate vector formula for y-axis (x-z plane): + +[latexmath] +++++ +\[ +\begin{aligned} +x' &= x\cos\theta - z\sin\theta \\ +y' &= y \\ +z' &= x\sin\theta + z\cos\theta +\end{aligned} \] +++++ + +Rotate vector formula for z-axis (x-y plane): + +[latexmath] +++++ +\[ +\begin{aligned} +x' &= x\cos\theta - y\sin\theta \\ +y' &= x\sin\theta + y\cos\theta \\ +z' &= z +\end{aligned} +\] +++++ + +== 2. Single-axis rotation matrices (Euler Angles) + +=== Rotation about X-axis (roll) + +[latexmath] +++++ +\boxed{ +R_x(\alpha) = +\begin{bmatrix} +1 & 0 & 0 \\ +0 & \cos\alpha & -\sin\alpha \\ +0 & \sin\alpha & \cos\alpha +\end{bmatrix} +} +++++ + +=== Rotation about Y-axis (pitch) + +[latexmath] +++++ +\boxed{ +R_y(\beta) = +\begin{bmatrix} +\cos\beta & 0 & \sin\beta \\ +0 & 1 & 0 \\ +-\sin\beta & 0 & \cos\beta +\end{bmatrix} +} +++++ + +=== Rotation about Z-axis (yaw) + +[latexmath] +++++ +\boxed{ +R_z(\gamma) = +\begin{bmatrix} +\cos\gamma & -\sin\gamma & 0 \\ +\sin\gamma & \cos\gamma & 0 \\ +0 & 0 & 1 +\end{bmatrix} +} +++++ + + +Euler angles will always have gimbal lock. You can switch which axis has gimbal +lock by switching the order. Most application use quaternions instead of eular +angles. + +== Quaternions +click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/quaternions[here] for an interactive example + +Quaternions allow you to rotate smoothly around multiple axes without gimbal +lock. Chat-gpt rewrote the code to use quaternions. Here are some of the +replys about https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternions.html;hb=HEAD[quaternions]. + +=== 1. What is a quaternion (in graphics)? + +A quaternion is a 4-component number: + +[latexmath] +++++ +q = (w, x, y, z) +++++ + +In 3D graphics, we use **unit quaternions** to represent **rotations**. + +A rotation by: + +* angle latexmath:[\theta] +* around a **unit axis** latexmath:[\vec{u} = (u_x, u_y, u_z)] + +is encoded as: + +[latexmath] +++++ +\boxed{ +q = \left( +\cos\frac{\theta}{2}, +\; u_x \sin\frac{\theta}{2}, +\; u_y \sin\frac{\theta}{2}, +\; u_z \sin\frac{\theta}{2} +\right) +} +++++ + +This is the **axis–angle -> quaternion** formula. + +==== Why half-angle? +Because quaternion rotation comes from complex numbers and Euler’s formula — +doubling happens naturally in the math. + +=== 2. quatFromAxisAngle + +[source, js] +---- +function quatFromAxisAngle(axis, angle) { + const h = angle / 2; + const s = Math.sin(h); + return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s]; +} +---- + +You’re directly implementing: + +[latexmath] +++++ +q = (\cos(\theta/2), \vec{u}\sin(\theta/2)) +++++ + +* latexmath:[h = angle / 2] -> half-angle +* latexmath:[cos(h)] → scalar part (w) +* latexmath:[axis * sin(h)] → vector part latexmath:[(x,y,z)] + +IMPORTANT: `axis` **must be normalized** (length = 1), otherwise rotation will + scale or skew. + +=== 3. Quaternion multiplication (rotation composition) + +[source, js] +---- +function quatMultiply(a, b) { + const [w1,x1,y1,z1] = a; + const [w2,x2,y2,z2] = b; + return [ + w1*w2 - x1*x2 - y1*y2 - z1*z2, + w1*x2 + x1*w2 + y1*z2 - z1*y2, + w1*y2 - x1*z2 + y1*w2 + z1*x2, + w1*z2 + x1*y2 - y1*x2 + z1*w2 + ]; +} +---- + +formula: + +[latexmath] +++++ +q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2) +++++ + +Quaternion multiplication is: + +[latexmath] +++++ +\boxed{ +q_1 q_2 = +\left( +w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,\; +w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2 +\right) +} +++++ + +Expanded component-wise -> exactly your code. + +==== Why this matters + +* Quaternion multiplication **combines rotations** +* Order matters: + +[latexmath] +++++ +q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}} +++++ + +This avoids **gimbal lock**, unlike Euler angles. + +=== 4. Quaternion conjugate + +[source, js] +---- +function quatConjugate(q) { + return [q[0], -q[1], -q[2], -q[3]]; +} +---- + +For: + +[latexmath] +++++ +q = (w, x, y, z) +++++ + +The conjugate is: + +[latexmath] +++++ +\boxed{ +q^* = (w, -x, -y, -z) +} +++++ + +=== Why this works + +If (q) is a **unit quaternion**: + +[latexmath] +++++ +q^{-1} = q^* +++++ + +So conjugate = **inverse rotation**. + +This is why cameras use conjugates — moving the world opposite to the camera +rotation. + +=== 5. Rotating a vector using quaternions + +[source, js] +---- +function quatRotateVector(q, v) { + const qv = [0, v.x, v.y, v.z]; + const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q)); + return { x: r[1], y: r[2], z: r[3] }; +} +---- + +The key formula + +To rotate a vector latexmath:[\vec{v}]: + +1. Convert it to a **pure quaternion** + +[latexmath] +++++ +v_q = (0, v_x, v_y, v_z) +++++ + +2. Apply: + +[latexmath] +++++ +\boxed{ +v' = q \; v_q \; q^{-1} +} +++++ + +3. Extract the vector part + +==== Why this works + +Quaternion rotation encodes: + +* axis +* angle +* orientation + +The sandwich product rotates without: + +* scaling +* skewing +* drifting + +=== Visual intuition + +A rotation quaternion represents: + +[latexmath] +++++ +q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k) +++++ + +This is the 3D analog of: + +[latexmath] +++++ +e^{i\theta} = \cos\theta + i\sin\theta +++++ + +== Camera + +chat-gpt replys about https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/camera.html;hb=HEAD[camera]. + +== Simple clickable object + +click https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=example.html;hb=refs/heads/clickable[here] for an interactive example + +This does not account for the rotation of the object but it does check to see if +your mouse is within the rectangular bounds of the mesh. + +1. finding the min and max values for x and y of the mesh: + +[source, js] +---- +function computeBounds(mesh) { + let min = {x: Infinity, y: Infinity} + let max = {x: -Infinity, y: -Infinity} + + for (const v of mesh) { + min.x = Math.min(min.x, v.x) + min.y = Math.min(min.y, v.y) + + max.x = Math.max(max.x, v.x) + max.y = Math.max(max.y, v.y) + } + return {min, max} +} +---- + +2. Define the box corners of the rect for the mesh + +[source, js] +---- +function boundingBoxCorners({min, max}) { + return { + bottom: {left: {x: min.x, y: max.y}, right: {x: max.x, y: max.y}}, + top: {left: {x: min.x, y: min.y}, right: {x: max.x, y: min.y}}, + + } +} +---- + +3. Check to see if the mouse is in the rect in the game loop. If it is, and the + mouse button is down, then you can move the object + +[source, js] +---- +inObject = ( + mousePos.x > rect.top.left.x + && + mousePos.x < rect.top.right.x + && + mousePos.y > rect.top.left.y + && + mousePos.y < rect.bottom.right.y +) + +if (mouseDown && inObject) canMoveObject = true + +if (!mouseDown) canMoveObject = false +---- + +== Links + +* https://www.youtube.com/watch?v=qjWkNZ0SXfo[One Formula That demystifies 3D Graphics] +* https://www.youtube.com/watch?v=EZufiIwwqFA[Rotation matrix derivation (step-by-step prove)] +* https://matthew-brett.github.io/teaching/rotation_2d.html[rotation 2D by mathew] +* https://www.youtube.com/watch?v=zc8b2Jo7mno[gimbal lock visual representation] +* https://www.youtube.com/watch?v=zjMuIxRvygQ[3Blue1Brown | Quaternions and 3d rotation, explained interactively] +* https://www.youtube.com/watch?v=d4EgbgTm0Bg&t=17s[3Blue1Brown | Visualizing the 4d numbers Quanternions] +* https://phas.ubc.ca/~berciu/TEACHING/PHYS206/LECTURES/FILES/euler.pdf[euler.pdf] diff --git a/docinfo.conf b/docinfo.conf new file mode 100644 index 0000000..fc4596c --- /dev/null +++ b/docinfo.conf @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +HOSTNAME=nyquistdev.net +DOCINFODIR=/usr/local/share/asciidoctor-styles/dark1/ diff --git a/docs/chatgpt-dump/Makehtml b/docs/chatgpt-dump/Makehtml new file mode 100755 index 0000000..615bffe --- /dev/null +++ b/docs/chatgpt-dump/Makehtml @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +PANDOC_OPTS=( + --from markdown+smart + --to html + --standalone + --metadata charset=utf-8 + --mathjax +) + +for md in *.md; do + html="${md%.md}.html" + pandoc "$md" "${PANDOC_OPTS[@]}" -o "$html" --mathjax +done diff --git a/docs/chatgpt-dump/camera.html b/docs/chatgpt-dump/camera.html new file mode 100644 index 0000000..833aff7 --- /dev/null +++ b/docs/chatgpt-dump/camera.html @@ -0,0 +1,1122 @@ + + + + + + + camera + + + +

Yes 👍 You’re very close already. What you currently have is +object movement, not a camera. We’ll add a real +camera while changing as little of your code as possible.

+

I’ll do this in small, precise steps, and I’ll +clearly mark what to add and what to +replace.

+
+

1️⃣ What “adding a +camera” means in your code

+

Right now your pipeline is:

+
vertex
+ → rotate cube
+ → translate cube (position)
+ → project
+ → screen
+

With a camera, it becomes:

+
vertex
+ → rotate cube
+ → translate cube (object position)
+ → apply camera (inverse transform)
+ → project
+ → screen
+

So we need:

+ +
+

2️⃣ Add a camera object (NEW)

+

Add this near your position variable:

+
let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1, 0, 0, 0] // identity quaternion
+};
+
+

3️⃣ Camera helper functions +(NEW)

+

Add these below your quaternion functions:

+
function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function applyCamera(p, cam) {
+    // inverse translation
+    let v = {
+        x: p.x - cam.position.x,
+        y: p.y - cam.position.y,
+        z: p.z - cam.position.z
+    };
+
+    // inverse rotation
+    const invRot = quatConjugate(cam.rotation);
+    return quatRotateVector(invRot, v);
+}
+

Why this works:

+ +
+

4️⃣ Modify your render loop +(IMPORTANT)

+

❌ OLD code (inside +frame()):

+
line(
+    screen(project(translate(quatRotateVector(q, a), position))),
+    screen(project(translate(quatRotateVector(q, b), position)))
+);
+

✅ REPLACE with this:

+
let ra = translate(quatRotateVector(q, a), position);
+let rb = translate(quatRotateVector(q, b), position);
+
+ra = applyCamera(ra, camera);
+rb = applyCamera(rb, camera);
+
+line(
+    screen(project(ra)),
+    screen(project(rb))
+);
+

That’s the entire camera integration.

+
+

5️⃣ Move the camera (instead +of the cube)

+

Right now your scroll code moves position. We’ll change +it to move the camera instead.

+
+

5.1 Forward / backward (scroll)

+

❌ OLD:

+
position.z += .03;
+position.z -= .03;
+

✅ REPLACE with:

+
camera.position.z += .03;
+camera.position.z -= .03;
+
+

5.2 Left / right (shift + scroll)

+

❌ OLD:

+
position.x += .004;
+position.x -= .004;
+

✅ REPLACE with:

+
camera.position.x += .04;
+camera.position.x -= .04;
+

Also remove this loop (it does nothing useful):

+
for (const v of vs) { ... }
+

You should never modify vertices for movement.

+
+

6️⃣ (Optional but +recommended) Rotate the camera

+

Add this helper:

+
function rotateCamera(axis, angle) {
+    const q = quatFromAxisAngle(axis, angle);
+    camera.rotation = quatMultiply(q, camera.rotation);
+}
+

Add keyboard look:

+
window.addEventListener("keydown", e => {
+    const rot = 0.03;
+
+    if (e.key === "ArrowLeft")  rotateCamera({x:0,y:1,z:0},  rot);
+    if (e.key === "ArrowRight") rotateCamera({x:0,y:1,z:0}, -rot);
+    if (e.key === "ArrowUp")    rotateCamera({x:1,y:0,z:0},  rot);
+    if (e.key === "ArrowDown")  rotateCamera({x:1,y:0,z:0}, -rot);
+});
+

Now you have true camera yaw & pitch.

+
+

7️⃣ What you have now +(important milestone)

+

You now have:

+

✔ Object-local rotation ✔ Object world position ✔ Camera position ✔ +Camera rotation ✔ World → view → projection pipeline

+

This is literally the same architecture used by:

+ +

You’re writing a software 3D renderer.

+
+

8️⃣ One key mental rule +(remember this)

+
+

Objects move themselves. Cameras move the world.

+
+

If something feels “backwards”, it usually means you forgot the +inverse.

+
+ +

1️⃣ FPS-style movement (WASD relative to camera) 2️⃣ Proper near-plane +clipping 3️⃣ Field-of-view control 4️⃣ Depth sorting / painter’s algorithm +5️⃣ Backface culling

+

If you want, say which one, and I’ll guide you +through it step by step.

+

Yes — below is a clean, complete rewrite of your entire +snippet that:

+ +

I’ve kept it very close to your original style, just +corrected architecturally.

+
+

✅ FULL WORKING CAMERA-BASED +VERSION

+
<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],
+    [1,5],
+    [2,6],
+    [3,7],
+];
+
+/* ===================== OBJECT ===================== */
+let objectPosition = { x: 0, y: 0, z: 2 };
+let angle = 0;
+let activeAxes = ["x"];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0] // quaternion
+};
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const inv = quatConjugate(q);
+    const t = quatMultiply(q, qv);
+    const r = quatMultiply(t, inv);
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== TRANSFORMS ===================== */
+function translate(p, t) {
+    return { x: p.x + t.x, y: p.y + t.y, z: p.z + t.z };
+}
+
+function applyCamera(p) {
+    let v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0, 0, game.width, game.height);
+
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("Click: change cube rotation axes", 10, 18);
+    ctx.fillText("Scroll: move camera forward/back", 10, 36);
+    ctx.fillText("Shift+Scroll: strafe camera", 10, 54);
+    ctx.fillText("Arrow keys: look around", 10, 72);
+    ctx.fillText("Axes: " + activeAxes.join(","), 10, 90);
+}
+
+function line(a, b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x, a.y);
+    ctx.lineTo(b.x, b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax === "x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax === "y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax === "z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i = 0; i < e.length; i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = translate(quatRotateVector(q, a), objectPosition);
+            b = translate(quatRotateVector(q, b), objectPosition);
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z <= NEAR || b.z <= NEAR) continue;
+
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+
+frame();
+
+/* ===================== INPUT ===================== */
+game.onclick = () => {
+    const modes = [["x"],["y"],["z"],["x","y"],["y","z"],["x","z"],["x","y","z"]];
+    activeAxes = modes[(modes.findIndex(m => m+"" === activeAxes+"") + 1) % modes.length];
+};
+
+game.addEventListener("wheel", e => {
+    e.preventDefault();
+    if (e.shiftKey) camera.position.x += e.deltaY * 0.002;
+    else camera.position.z += e.deltaY * 0.01;
+}, { passive:false });
+
+window.addEventListener("keydown", e => {
+    const r = 0.04;
+    if (e.key === "ArrowLeft")  camera.rotation = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0},  r), camera.rotation);
+    if (e.key === "ArrowRight") camera.rotation = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, -r), camera.rotation);
+    if (e.key === "ArrowUp")    camera.rotation = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0},  r), camera.rotation);
+    if (e.key === "ArrowDown")  camera.rotation = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, -r), camera.rotation);
+});
+</script>
+
+

🎯 What this version gives you

+

✔ True camera system ✔ Object rotation separate from +camera ✔ Proper world → view → projection pipeline ✔ No +vertex mutation ✔ No fake camera tricks ✔ Clean mental model identical +to real engines

+
+

🚀 You are now at the level +where:

+ +

If you want next, I strongly recommend: 👉 FPS-style movement +(WASD relative to camera direction) 👉 +Mouse-look 👉 Perspective FOV math

+

Just say which one 👍

+

Absolutely — this is the correct next step 👍 “Fly +mode” means:

+ +

Below is a full rewrite of the previous camera-based +code, upgraded to a true fly camera. Nothing is faked, +and the math matches real engines.

+
+

✅ FULL FLY-CAMERA +VERSION (WASD + Mouse Look)

+
<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
+];
+
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
+let activeAxes = ["x","y"];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0], // quaternion
+    speed: 0.05,
+    sensitivity: 0.002
+};
+
+/* ===================== INPUT ===================== */
+const keys = {};
+window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
+window.addEventListener("keyup",   e => keys[e.key.toLowerCase()] = false);
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== CAMERA MATH ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
+
+function applyCamera(p) {
+    let v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0,0,game.width,game.height);
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("WASD: move | QE: up/down", 10, 18);
+    ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
+}
+
+function line(a,b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x,a.y);
+    ctx.lineTo(b.x,b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    // cube rotation
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    // camera movement
+    if (keys["w"]) {
+        const f = cameraForward();
+        camera.position.x += f.x * camera.speed;
+        camera.position.y += f.y * camera.speed;
+        camera.position.z += f.z * camera.speed;
+    }
+    if (keys["s"]) {
+        const f = cameraForward();
+        camera.position.x -= f.x * camera.speed;
+        camera.position.y -= f.y * camera.speed;
+        camera.position.z -= f.z * camera.speed;
+    }
+    if (keys["a"]) {
+        const r = cameraRight();
+        camera.position.x -= r.x * camera.speed;
+        camera.position.y -= r.y * camera.speed;
+        camera.position.z -= r.z * camera.speed;
+    }
+    if (keys["d"]) {
+        const r = cameraRight();
+        camera.position.x += r.x * camera.speed;
+        camera.position.y += r.y * camera.speed;
+        camera.position.z += r.z * camera.speed;
+    }
+    if (keys["q"]) {
+        const u = cameraUp();
+        camera.position.x -= u.x * camera.speed;
+        camera.position.y -= u.y * camera.speed;
+        camera.position.z -= u.z * camera.speed;
+    }
+    if (keys["e"]) {
+        const u = cameraUp();
+        camera.position.x += u.x * camera.speed;
+        camera.position.y += u.y * camera.speed;
+        camera.position.z += u.z * camera.speed;
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i=0;i<e.length;i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = quatRotateVector(q, a);
+            b = quatRotateVector(q, b);
+
+            a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
+            b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+frame();
+
+/* ===================== MOUSE LOOK ===================== */
+game.onclick = () => game.requestPointerLock();
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    const yaw   = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
+    const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});
+</script>
+
+

🎯 What you now have

+

✔ True fly camera ✔ Movement relative to view +direction ✔ Mouse-look yaw + pitch ✔ Quaternion-based, no gimbal lock ✔ +Identical behavior to Unity / Unreal fly mode

+

This is real engine-level camera control.

+
+ +

1️⃣ Clamp pitch to avoid flipping 2️⃣ Add roll (airplane-style) 3️⃣ Add +acceleration / inertia 4️⃣ Add multiple objects + scene graph 5️⃣ Replace +projection with real FOV math

+

If you want any of those, say which one — you’re doing excellent +work.

+

Yes — below is a full rewrite of the fly-camera +version that keeps the ability to change which axes the +cube rotates on, exactly like your earlier versions.

+

Nothing is removed:

+ +
+

✅ FULL FLY-CAMERA ++ ROTATION-AXIS TOGGLE VERSION

+
<canvas id="game"></canvas>
+<script>
+/* ===================== SETUP ===================== */
+const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+const ctx = game.getContext("2d");
+
+const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+
+const FPS = 60;
+const NEAR = 0.1;
+
+/* ===================== GEOMETRY ===================== */
+const vertices = [
+    {x:  0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y:  0.25, z:  0.25},
+    {x: -0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y: -0.25, z:  0.25},
+    {x:  0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y:  0.25, z: -0.25},
+    {x: -0.25, y: -0.25, z: -0.25},
+    {x:  0.25, y: -0.25, z: -0.25},
+];
+
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
+];
+
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
+
+const AXIS_MODES = [
+    ["x"], ["y"], ["z"],
+    ["x","y"], ["y","z"], ["x","z"],
+    ["x","y","z"]
+];
+let axisModeIndex = 0;
+let activeAxes = AXIS_MODES[axisModeIndex];
+
+/* ===================== CAMERA ===================== */
+let camera = {
+    position: { x: 0, y: 0, z: 0 },
+    rotation: [1,0,0,0],
+    speed: 0.05,
+    sensitivity: 0.002
+};
+
+/* ===================== INPUT ===================== */
+const keys = {};
+window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
+window.addEventListener("keyup",   e => keys[e.key.toLowerCase()] = false);
+
+/* ===================== QUATERNIONS ===================== */
+function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+/* ===================== CAMERA VECTORS ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
+
+function applyCamera(p) {
+    const v = {
+        x: p.x - camera.position.x,
+        y: p.y - camera.position.y,
+        z: p.z - camera.position.z
+    };
+    return quatRotateVector(quatConjugate(camera.rotation), v);
+}
+
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
+
+/* ===================== DRAW ===================== */
+function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0,0,game.width,game.height);
+
+    ctx.fillStyle = TEXT;
+    ctx.font = "14px monospace";
+    ctx.fillText("WASD: move | QE: up/down", 10, 18);
+    ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
+    ctx.fillText("Click canvas again: change cube axes", 10, 54);
+    ctx.fillText("Cube axes: " + activeAxes.join(","), 10, 72);
+}
+
+function line(a,b) {
+    ctx.strokeStyle = FOREGROUND;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(a.x,a.y);
+    ctx.lineTo(b.x,b.y);
+    ctx.stroke();
+}
+
+/* ===================== LOOP ===================== */
+function frame() {
+    angle += Math.PI / FPS;
+
+    // --- cube rotation quaternion ---
+    let q = [1,0,0,0];
+    for (const ax of activeAxes) {
+        if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
+        if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
+        if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+    }
+
+    // --- camera movement ---
+    if (keys["w"]) {
+        const f = cameraForward();
+        camera.position.x += f.x * camera.speed;
+        camera.position.y += f.y * camera.speed;
+        camera.position.z += f.z * camera.speed;
+    }
+    if (keys["s"]) {
+        const f = cameraForward();
+        camera.position.x -= f.x * camera.speed;
+        camera.position.y -= f.y * camera.speed;
+        camera.position.z -= f.z * camera.speed;
+    }
+    if (keys["a"]) {
+        const r = cameraRight();
+        camera.position.x -= r.x * camera.speed;
+        camera.position.y -= r.y * camera.speed;
+        camera.position.z -= r.z * camera.speed;
+    }
+    if (keys["d"]) {
+        const r = cameraRight();
+        camera.position.x += r.x * camera.speed;
+        camera.position.y += r.y * camera.speed;
+        camera.position.z += r.z * camera.speed;
+    }
+    if (keys["q"]) {
+        const u = cameraUp();
+        camera.position.x -= u.x * camera.speed;
+        camera.position.y -= u.y * camera.speed;
+        camera.position.z -= u.z * camera.speed;
+    }
+    if (keys["e"]) {
+        const u = cameraUp();
+        camera.position.x += u.x * camera.speed;
+        camera.position.y += u.y * camera.speed;
+        camera.position.z += u.z * camera.speed;
+    }
+
+    clear();
+
+    for (const e of edges) {
+        for (let i=0;i<e.length;i++) {
+            let a = vertices[e[i]];
+            let b = vertices[e[(i+1)%e.length]];
+
+            a = quatRotateVector(q, a);
+            b = quatRotateVector(q, b);
+
+            a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
+            b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+
+            a = applyCamera(a);
+            b = applyCamera(b);
+
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
+
+    setTimeout(frame, 1000/FPS);
+}
+frame();
+
+/* ===================== INTERACTION ===================== */
+game.onclick = () => {
+    game.requestPointerLock();
+    axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length;
+    activeAxes = AXIS_MODES[axisModeIndex];
+};
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    const yaw   = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
+    const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});
+</script>
+
+

🧠 What this version +teaches (important)

+ +

This separation is exactly how real engines +work.

+
+ +

1️⃣ Clamp camera pitch (avoid flipping) 2️⃣ Add roll for airplane +controls 3️⃣ Add multiple objects 4️⃣ Add face culling 5️⃣ Add real +perspective FOV

+

If you want any of those, tell me which — you’re doing real 3D +graphics now.

+ + diff --git a/docs/chatgpt-dump/camera.md b/docs/chatgpt-dump/camera.md new file mode 100644 index 0000000..5be4a45 --- /dev/null +++ b/docs/chatgpt-dump/camera.md @@ -0,0 +1,987 @@ +Yes 👍 +You’re *very close already*. What you currently have is **object movement**, not a camera. +We’ll add a **real camera** while changing as little of your code as possible. + +I’ll do this in **small, precise steps**, and I’ll clearly mark **what to add** and **what to replace**. + +--- + +# 1️⃣ What “adding a camera” means in your code + +Right now your pipeline is: + +``` +vertex + → rotate cube + → translate cube (position) + → project + → screen +``` + +With a camera, it becomes: + +``` +vertex + → rotate cube + → translate cube (object position) + → apply camera (inverse transform) + → project + → screen +``` + +So we need: + +* a **camera position** +* a **camera rotation (quaternion)** +* a function that applies the **inverse camera transform** + +--- + +# 2️⃣ Add a camera object (NEW) + +Add this near your `position` variable: + +```js +let camera = { + position: { x: 0, y: 0, z: 0 }, + rotation: [1, 0, 0, 0] // identity quaternion +}; +``` + +--- + +# 3️⃣ Camera helper functions (NEW) + +Add these **below your quaternion functions**: + +```js +function quatConjugate(q) { + return [q[0], -q[1], -q[2], -q[3]]; +} + +function applyCamera(p, cam) { + // inverse translation + let v = { + x: p.x - cam.position.x, + y: p.y - cam.position.y, + z: p.z - cam.position.z + }; + + // inverse rotation + const invRot = quatConjugate(cam.rotation); + return quatRotateVector(invRot, v); +} +``` + +Why this works: + +* Camera movement = **world moves opposite** +* Camera rotation = **world rotates inverse** + +--- + +# 4️⃣ Modify your render loop (IMPORTANT) + +### ❌ OLD code (inside `frame()`): + +```js +line( + screen(project(translate(quatRotateVector(q, a), position))), + screen(project(translate(quatRotateVector(q, b), position))) +); +``` + +### ✅ REPLACE with this: + +```js +let ra = translate(quatRotateVector(q, a), position); +let rb = translate(quatRotateVector(q, b), position); + +ra = applyCamera(ra, camera); +rb = applyCamera(rb, camera); + +line( + screen(project(ra)), + screen(project(rb)) +); +``` + +That’s the **entire camera integration**. + +--- + +# 5️⃣ Move the camera (instead of the cube) + +Right now your scroll code moves `position`. +We’ll change it to move **the camera instead**. + +--- + +## 5.1 Forward / backward (scroll) + +### ❌ OLD: + +```js +position.z += .03; +position.z -= .03; +``` + +### ✅ REPLACE with: + +```js +camera.position.z += .03; +camera.position.z -= .03; +``` + +--- + +## 5.2 Left / right (shift + scroll) + +### ❌ OLD: + +```js +position.x += .004; +position.x -= .004; +``` + +### ✅ REPLACE with: + +```js +camera.position.x += .04; +camera.position.x -= .04; +``` + +Also remove this loop (it does nothing useful): + +```js +for (const v of vs) { ... } +``` + +You should **never modify vertices for movement**. + +--- + +# 6️⃣ (Optional but recommended) Rotate the camera + +Add this helper: + +```js +function rotateCamera(axis, angle) { + const q = quatFromAxisAngle(axis, angle); + camera.rotation = quatMultiply(q, camera.rotation); +} +``` + +Add keyboard look: + +```js +window.addEventListener("keydown", e => { + const rot = 0.03; + + if (e.key === "ArrowLeft") rotateCamera({x:0,y:1,z:0}, rot); + if (e.key === "ArrowRight") rotateCamera({x:0,y:1,z:0}, -rot); + if (e.key === "ArrowUp") rotateCamera({x:1,y:0,z:0}, rot); + if (e.key === "ArrowDown") rotateCamera({x:1,y:0,z:0}, -rot); +}); +``` + +Now you have **true camera yaw & pitch**. + +--- + +# 7️⃣ What you have now (important milestone) + +You now have: + +✔ Object-local rotation +✔ Object world position +✔ Camera position +✔ Camera rotation +✔ World → view → projection pipeline + +This is **literally the same architecture** used by: + +* OpenGL +* WebGL +* Unity +* Unreal + +You’re writing a **software 3D renderer**. + +--- + +# 8️⃣ One key mental rule (remember this) + +> Objects move themselves. +> Cameras move the world. + +If something feels “backwards”, it usually means you forgot the inverse. + +--- + +## 🔥 Next things you can add (recommended order) + +1️⃣ FPS-style movement (WASD relative to camera) +2️⃣ Proper near-plane clipping +3️⃣ Field-of-view control +4️⃣ Depth sorting / painter’s algorithm +5️⃣ Backface culling + +If you want, say **which one**, and I’ll guide you through it step by step. + +Yes — below is a **clean, complete rewrite of your entire snippet** that: + +* Uses a **real camera** (position + quaternion rotation) +* Keeps your **cube rotation via quaternions** +* Uses **scroll + shift+scroll** for camera movement +* Uses **arrow keys to rotate the camera** +* Uses a **proper 3D pipeline** (object → world → view → projection) + +I’ve kept it **very close to your original style**, just corrected architecturally. + +--- + +## ✅ FULL WORKING CAMERA-BASED VERSION + +```html + + +``` + +--- + +## 🎯 What this version gives you + +✔ True **camera system** +✔ Object rotation separate from camera +✔ Proper **world → view → projection** pipeline +✔ No vertex mutation +✔ No fake camera tricks +✔ Clean mental model identical to real engines + +--- + +### 🚀 You are now at the level where: + +* WebGL will suddenly make sense +* Matrices will feel intuitive +* Game engines won’t feel “magical” anymore + +If you want next, I strongly recommend: +👉 **FPS-style movement (WASD relative to camera direction)** +👉 **Mouse-look** +👉 **Perspective FOV math** + +Just say which one 👍 + +Absolutely — this is the **correct next step** 👍 +“Fly mode” means: + +* **W / S** → move forward / backward **relative to where the camera is facing** +* **A / D** → strafe left / right +* **Q / E** → move down / up +* **Mouse** → look around (yaw + pitch) +* The cube stays in the world; **the camera flies** + +Below is a **full rewrite** of the previous camera-based code, upgraded to a **true fly camera**. +Nothing is faked, and the math matches real engines. + +--- + +## ✅ FULL FLY-CAMERA VERSION (WASD + Mouse Look) + +```html + + +``` + +--- + +## 🎯 What you now have + +✔ **True fly camera** +✔ Movement relative to view direction +✔ Mouse-look yaw + pitch +✔ Quaternion-based, no gimbal lock +✔ Identical behavior to Unity / Unreal fly mode + +This is *real engine-level camera control*. + +--- + +### 🔥 Next upgrades (strongly recommended) + +1️⃣ Clamp pitch to avoid flipping +2️⃣ Add roll (airplane-style) +3️⃣ Add acceleration / inertia +4️⃣ Add multiple objects + scene graph +5️⃣ Replace projection with real FOV math + +If you want any of those, say which one — you’re doing excellent work. + +Yes — below is a **full rewrite of the fly-camera version** that **keeps the ability to change which axes the cube rotates on**, exactly like your earlier versions. + +Nothing is removed: + +* ✅ Fly camera (WASD + QE + mouse look) +* ✅ Quaternion camera +* ✅ Quaternion cube rotation +* ✅ **Click to cycle cube rotation axes** +* ✅ Clean, correct 3D pipeline + +--- + +## ✅ FULL FLY-CAMERA + ROTATION-AXIS TOGGLE VERSION + +```html + + +``` + +--- + +## 🧠 What this version teaches (important) + +* Cube rotation is **object-local** +* Camera rotation is **view-space** +* Both use **quaternions**, but for different purposes +* Axis toggling affects **only the cube**, never the camera + +This separation is **exactly** how real engines work. + +--- + +## 🚀 Strong next steps (recommended) + +1️⃣ Clamp camera pitch (avoid flipping) +2️⃣ Add roll for airplane controls +3️⃣ Add multiple objects +4️⃣ Add face culling +5️⃣ Add real perspective FOV + +If you want any of those, tell me which — you’re doing *real 3D graphics now*. + diff --git a/docs/chatgpt-dump/euler-angle.html b/docs/chatgpt-dump/euler-angle.html new file mode 100644 index 0000000..5f3ed29 --- /dev/null +++ b/docs/chatgpt-dump/euler-angle.html @@ -0,0 +1,460 @@ + + + + + + + euler-angle + + + + + +

Absolutely. I’ll give you the actual math formulas, +show the rotation matrices, explain the order +issue, and relate them directly to what you’ve been doing with +quaternions.

+
+

1. What are Euler angles?

+

Euler angles describe a 3D rotation as three +sequential rotations about coordinate axes.

+

Typically:

+ +

We’ll denote:

+ +

⚠️ Order matters. XYZ is not +the same as ZYX.

+
+

2. Single-axis rotation +matrices

+

Rotation about X-axis (roll)

+

[ & & \end{bmatrix} }]

+
+

Rotation about Y-axis (pitch)

+

[ & 0 & \end{bmatrix} }]

+
+

Rotation about Z-axis (yaw)

+

[ & 0 & 1 \end{bmatrix} }]

+
+

3. Combined Euler rotation +(XYZ order)

+

If you rotate X → then Y → then Z:

+

[ ]

+

Matrix multiplication is right to left (apply X +first).

+

Expanded matrix:

+

[ ]

+

Where:

+ +
+

4. Rotating a vector with +Euler angles

+

Given a vector:

+

[ = ]

+

Rotated vector:

+

[ ]

+
+

5. Euler angles as equations +(explicit)

+

If using yaw–pitch–roll (Z–Y–X):

+

[ ]

+

This is what people usually implement directly (and +regret later).

+
+

6. Why Euler angles +cause problems (gimbal lock)

+

If: [ = ^]

+

Then:

+ +

This is gimbal lock.

+

Your quaternion code avoids this entirely.

+
+

7. Euler → Quaternion +conversion

+

Given Euler angles ( (, , ) ):

+

[ _z &= - \end{aligned} }]

+

(This is why quaternions feel scary — but they’re actually simpler in +practice.)

+
+

8. When Euler angles are +still useful

+

Euler angles are fine for:

+ +

But not for:

+ +
+

9. Key mental takeaway

+
+

Euler angles describe how you rotate. Quaternions +describe what rotation you have.

+
+

That’s why engines store quaternions internally but expose Euler +angles in editors.

+
+

10. Great references

+ +
+

If you want next:

+ +

Just say the word.

+

Great question — this is exactly where rotation matrices stop being +“symbols on a page” and start making sense.

+

I’ll walk you through how to actually do the math, +step by step, for rotation about the X-axis, and show +why the formula looks the way it does.

+
+

1. The X-axis rotation matrix

+

Rotation by angle ( ) around the X-axis:

+

[ R_x() = ]

+

This matrix rotates points in the YZ-plane, leaving +X unchanged.

+
+

2. The vector you’re rotating

+

Let your 3D point be:

+

[ = ]

+

To rotate it:

+

[ ]

+
+

3. Matrix multiplication +rule (important)

+

When multiplying a matrix by a vector:

+[ +\[\begin{bmatrix} +a & b & c \ +d & e & f \ +g & h & i +\end{bmatrix}\] +g & h & i \end{bmatrix} +\[\begin{bmatrix} +x \ y \ z +\end{bmatrix}\] +

=============

+\[\begin{bmatrix} +ax + by + cz \ +dx + ey + fz \ +gx + hy + iz +\end{bmatrix}\] +


+gx + hy + iz \end{bmatrix} ]

+

Each row produces one output component.

+
+

4. Multiply (R_x()) by the +vector

+

First row → (x’)

+

[ x’ = 1x + 0y + 0z = x]

+

✔ X stays the same

+
+

Second row → (y’)

+

[ y’ = 0x + y + (-)z]

+

[ ]

+
+

Third row → (z’)

+

[ z’ = 0x + y + z]

+

[ ]

+
+

5. Final result (important)

+

[ ’ &= y+ z \end{aligned} }]

+

This is the explicit math behind X-axis +rotation.

+
+

6. Why this makes geometric +sense

+

Imagine looking down the +X-axis

+

You see the YZ-plane as a flat 2D plane.

+

The matrix:

+

[ ]

+

is just the 2D rotation matrix, applied to +(y, z).

+

So rotating around X in 3D = rotating (y, z) in 2D.

+
+

7. Connection to your +quaternion code

+

This line in your earlier code:

+
y = y*c - z*s
+z = y*s + z*c
+

is exactly this matrix multiplication.

+

Quaternions just wrap this logic in a more stable form.

+
+

8. Quick numeric example

+

Rotate point (0, 1, 0) by 90° (π/2) +around X:

+

[ (90)=0,(90)=1]

+

[ ]

+

Result:

+

[ (0,0,1)]

+

✔ Point moved from Y-axis to Z-axis

+
+

9. Key mental model

+
+

A rotation matrix does not move axes. It +re-expresses the point in a rotated coordinate system.

+
+
+

If you want next, I can show:

+ +

Just tell me 👍

+ + diff --git a/docs/chatgpt-dump/euler-angle.md b/docs/chatgpt-dump/euler-angle.md new file mode 100644 index 0000000..1e3d20a --- /dev/null +++ b/docs/chatgpt-dump/euler-angle.md @@ -0,0 +1,443 @@ +Absolutely. I’ll give you the **actual math formulas**, show the **rotation matrices**, explain the **order issue**, and relate them directly to what you’ve been doing with quaternions. + +--- + +## 1. What are Euler angles? + +**Euler angles** describe a 3D rotation as **three sequential rotations** about coordinate axes. + +Typically: + +* **Roll** → rotation about X +* **Pitch** → rotation about Y +* **Yaw** → rotation about Z + +We’ll denote: + +* ( \alpha ) = rotation about X (roll) +* ( \beta ) = rotation about Y (pitch) +* ( \gamma ) = rotation about Z (yaw) + +⚠️ **Order matters**. +`XYZ` is *not* the same as `ZYX`. + +--- + +## 2. Single-axis rotation matrices + +### Rotation about X-axis (roll) + +[ +\boxed{ +R_x(\alpha) = +\begin{bmatrix} +1 & 0 & 0 \ +0 & \cos\alpha & -\sin\alpha \ +0 & \sin\alpha & \cos\alpha +\end{bmatrix} +} +] + +--- + +### Rotation about Y-axis (pitch) + +[ +\boxed{ +R_y(\beta) = +\begin{bmatrix} +\cos\beta & 0 & \sin\beta \ +0 & 1 & 0 \ +-\sin\beta & 0 & \cos\beta +\end{bmatrix} +} +] + +--- + +### Rotation about Z-axis (yaw) + +[ +\boxed{ +R_z(\gamma) = +\begin{bmatrix} +\cos\gamma & -\sin\gamma & 0 \ +\sin\gamma & \cos\gamma & 0 \ +0 & 0 & 1 +\end{bmatrix} +} +] + +--- + +## 3. Combined Euler rotation (XYZ order) + +If you rotate **X → then Y → then Z**: + +[ +\boxed{ +R = R_z(\gamma) ; R_y(\beta) ; R_x(\alpha) +} +] + +Matrix multiplication is **right to left** (apply X first). + +Expanded matrix: + +[ +\begin{bmatrix} +c_\gamma c_\beta & +c_\gamma s_\beta s_\alpha - s_\gamma c_\alpha & +c_\gamma s_\beta c_\alpha + s_\gamma s_\alpha \ + +s_\gamma c_\beta & +s_\gamma s_\beta s_\alpha + c_\gamma c_\alpha & +s_\gamma s_\beta c_\alpha - c_\gamma s_\alpha \ + +-s_\beta & +c_\beta s_\alpha & +c_\beta c_\alpha +\end{bmatrix} +] + +Where: + +* ( c_x = \cos(x) ) +* ( s_x = \sin(x) ) + +--- + +## 4. Rotating a vector with Euler angles + +Given a vector: + +[ +\vec{v} = +\begin{bmatrix} +x \ y \ z +\end{bmatrix} +] + +Rotated vector: + +[ +\boxed{ +\vec{v}' = R \vec{v} +} +] + +--- + +## 5. Euler angles as equations (explicit) + +If using **yaw–pitch–roll (Z–Y–X)**: + +[ +\begin{aligned} +x' &= x(\cos\gamma\cos\beta) ++ y(\cos\gamma\sin\beta\sin\alpha - \sin\gamma\cos\alpha) ++ z(\cos\gamma\sin\beta\cos\alpha + \sin\gamma\sin\alpha) \ +y' &= x(\sin\gamma\cos\beta) ++ y(\sin\gamma\sin\beta\sin\alpha + \cos\gamma\cos\alpha) ++ z(\sin\gamma\sin\beta\cos\alpha - \cos\gamma\sin\alpha) \ +z' &= x(-\sin\beta) ++ y(\cos\beta\sin\alpha) ++ z(\cos\beta\cos\alpha) +\end{aligned} +] + +This is what people usually implement **directly** (and regret later). + +--- + +## 6. Why Euler angles cause problems (gimbal lock) + +If: +[ +\beta = \pm 90^\circ +] + +Then: + +* X and Z axes align +* You lose one degree of freedom +* Rotations become unstable + +This is **gimbal lock**. + +Your quaternion code avoids this entirely. + +--- + +## 7. Euler → Quaternion conversion + +Given Euler angles ( (\alpha, \beta, \gamma) ): + +[ +\boxed{ +\begin{aligned} +q_w &= \cos\frac{\alpha}{2}\cos\frac{\beta}{2}\cos\frac{\gamma}{2} ++ \sin\frac{\alpha}{2}\sin\frac{\beta}{2}\sin\frac{\gamma}{2} \ +q_x &= \sin\frac{\alpha}{2}\cos\frac{\beta}{2}\cos\frac{\gamma}{2} +- \cos\frac{\alpha}{2}\sin\frac{\beta}{2}\sin\frac{\gamma}{2} \ +q_y &= \cos\frac{\alpha}{2}\sin\frac{\beta}{2}\cos\frac{\gamma}{2} ++ \sin\frac{\alpha}{2}\cos\frac{\beta}{2}\sin\frac{\gamma}{2} \ +q_z &= \cos\frac{\alpha}{2}\cos\frac{\beta}{2}\sin\frac{\gamma}{2} +- \sin\frac{\alpha}{2}\sin\frac{\beta}{2}\cos\frac{\gamma}{2} +\end{aligned} +} +] + +(This is why quaternions feel scary — but they’re actually simpler in practice.) + +--- + +## 8. When Euler angles are still useful + +Euler angles are fine for: + +* UI sliders +* Editing tools +* Debugging +* Serialization + +But **not** for: + +* Camera motion +* Free rotation +* Animation interpolation + +--- + +## 9. Key mental takeaway + +> Euler angles describe **how** you rotate. +> Quaternions describe **what rotation you have**. + +That’s why engines store quaternions internally but expose Euler angles in editors. + +--- + +## 10. Great references + +* Wikipedia (math-heavy but correct): + [https://en.wikipedia.org/wiki/Euler_angles](https://en.wikipedia.org/wiki/Euler_angles) +* LearnOpenGL (practical): + [https://learnopengl.com/Getting-started/Camera](https://learnopengl.com/Getting-started/Camera) +* 3Blue1Brown (intuition): + [https://www.youtube.com/watch?v=zc8b2Jo7mno](https://www.youtube.com/watch?v=zc8b2Jo7mno) + +--- + +If you want next: + +* 🔁 Euler ↔ Quaternion code +* 🎥 Camera math explained geometrically +* 🧠 Why order matters visually +* 🧩 Gimbal lock demo + +Just say the word. + +Great question — this is exactly where rotation matrices stop being “symbols on a page” and start making sense. + +I’ll walk you through **how to actually do the math**, step by step, for **rotation about the X-axis**, and show **why the formula looks the way it does**. + +--- + +# 1. The X-axis rotation matrix + +Rotation by angle ( \alpha ) around the **X-axis**: + +[ +R_x(\alpha) = +\begin{bmatrix} +1 & 0 & 0 \ +0 & \cos\alpha & -\sin\alpha \ +0 & \sin\alpha & \cos\alpha +\end{bmatrix} +] + +This matrix rotates points in the **YZ-plane**, leaving X unchanged. + +--- + +# 2. The vector you’re rotating + +Let your 3D point be: + +[ +\vec{v} = +\begin{bmatrix} +x \ y \ z +\end{bmatrix} +] + +To rotate it: + +[ +\boxed{ +\vec{v}' = R_x(\alpha)\vec{v} +} +] + +--- + +# 3. Matrix multiplication rule (important) + +When multiplying a matrix by a vector: + +[ +\begin{bmatrix} +a & b & c \ +d & e & f \ +g & h & i +\end{bmatrix} +\begin{bmatrix} +x \ y \ z +\end{bmatrix} +============= + +\begin{bmatrix} +ax + by + cz \ +dx + ey + fz \ +gx + hy + iz +\end{bmatrix} +] + +Each **row** produces one output component. + +--- + +# 4. Multiply (R_x(\alpha)) by the vector + +### First row → (x') + +[ +x' = 1\cdot x + 0\cdot y + 0\cdot z = x +] + +✔ X stays the same + +--- + +### Second row → (y') + +[ +y' = 0\cdot x + \cos\alpha \cdot y + (-\sin\alpha)\cdot z +] + +[ +\boxed{ +y' = y\cos\alpha - z\sin\alpha +} +] + +--- + +### Third row → (z') + +[ +z' = 0\cdot x + \sin\alpha \cdot y + \cos\alpha \cdot z +] + +[ +\boxed{ +z' = y\sin\alpha + z\cos\alpha +} +] + +--- + +# 5. Final result (important) + +[ +\boxed{ +\begin{aligned} +x' &= x \ +y' &= y\cos\alpha - z\sin\alpha \ +z' &= y\sin\alpha + z\cos\alpha +\end{aligned} +} +] + +This is the **explicit math** behind X-axis rotation. + +--- + +# 6. Why this makes geometric sense + +### Imagine looking down the X-axis + +You see the **YZ-plane** as a flat 2D plane. + +The matrix: + +[ +\begin{bmatrix} +\cos\alpha & -\sin\alpha \ +\sin\alpha & \cos\alpha +\end{bmatrix} +] + +is just the **2D rotation matrix**, applied to `(y, z)`. + +So rotating around X in 3D = rotating `(y, z)` in 2D. + +--- + +# 7. Connection to your quaternion code + +This line in your earlier code: + +```js +y = y*c - z*s +z = y*s + z*c +``` + +is **exactly** this matrix multiplication. + +Quaternions just wrap this logic in a more stable form. + +--- + +# 8. Quick numeric example + +Rotate point `(0, 1, 0)` by **90° (π/2)** around X: + +[ +\cos(90^\circ)=0,\quad \sin(90^\circ)=1 +] + +[ +\begin{aligned} +x' &= 0 \ +y' &= 1\cdot0 - 0\cdot1 = 0 \ +z' &= 1\cdot1 + 0\cdot0 = 1 +\end{aligned} +] + +Result: + +[ +(0,0,1) +] + +✔ Point moved from Y-axis to Z-axis + +--- + +# 9. Key mental model + +> A rotation matrix **does not move axes**. +> It re-expresses the point in a rotated coordinate system. + +--- + +If you want next, I can show: + +* Y-axis and Z-axis derivations (they follow the same logic) +* Why the sine signs flip +* How to derive rotation matrices from scratch +* How this turns into quaternion math + +Just tell me 👍 diff --git a/docs/chatgpt-dump/quaternion-rotation.png b/docs/chatgpt-dump/quaternion-rotation.png new file mode 100644 index 0000000..06eddd8 Binary files /dev/null and b/docs/chatgpt-dump/quaternion-rotation.png differ diff --git a/docs/chatgpt-dump/quaternions.html b/docs/chatgpt-dump/quaternions.html new file mode 100644 index 0000000..35f63d2 --- /dev/null +++ b/docs/chatgpt-dump/quaternions.html @@ -0,0 +1,809 @@ + + + + + + + quaternions + + + +

Absolutely! I can show you how to incorporate +quaternions into your code to allow smooth rotation +around multiple axes at once, avoiding the limitations +of your current rotate function.

+

Let’s go step by step.

+
+

1. Why Quaternions +Here

+

Currently, you rotate around a single axis per frame:

+
rotate(a, rotationAxis, angle)
+

With quaternions, you can combine rotations around multiple +axes seamlessly. For example, you could rotate simultaneously +around X and Y.

+
+

2. Quaternion Basics

+

A quaternion can be represented as:

+

[ q = w + xi + yj + zk]

+

For 3D rotation:

+
q = [w, x, y, z]
+ +

To rotate a point v:

+
    +
  1. Convert v to a quaternion +[0, vx, vy, vz].
  2. +
  3. Apply v' = q * v * q⁻¹.
  4. +
+
+

3. Quaternion Utilities in +JS

+
function quatFromAxisAngle(axis, angle) {
+    const half = angle / 2
+    const s = Math.sin(half)
+    return [
+        Math.cos(half),   // w
+        axis.x * s,       // x
+        axis.y * s,       // y
+        axis.z * s        // z
+    ]
+}
+
+function quatMultiply(q1, q2) {
+    const [w1,x1,y1,z1] = q1
+    const [w2,x2,y2,z2] = q2
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ]
+}
+
+function quatRotateVector(q, v) {
+    const [w,x,y,z] = q
+    const [vx,vy,vz] = [v.x,v.y,v.z]
+
+    // Quaternion multiplication: v' = q * v_quat * q⁻¹
+    const qv = [0, vx, vy, vz]
+    const qConj = [w, -x, -y, -z]
+
+    const temp = quatMultiply(q, qv)
+    const result = quatMultiply(temp, qConj)
+    return { x: result[1], y: result[2], z: result[3] }
+}
+
+

4. Using Quaternions in +Your Frame

+

Instead of your rotate function:

+
const angleX = angle; 
+const angleY = angle/2; // different speed for demo
+
+const qx = quatFromAxisAngle({x:1,y:0,z:0}, angleX)
+const qy = quatFromAxisAngle({x:0,y:1,z:0}, angleY)
+
+const q = quatMultiply(qy, qx) // combined rotation
+
+for (const f of fs) {
+    for (let i = 0; i < f.length; ++i) {
+        const a = vs[f[i]]
+        const b = vs[f[(i + 1)%f.length]]
+
+        line(
+            screen(project(translateZ(quatRotateVector(q, a), dz))),
+            screen(project(translateZ(quatRotateVector(q, b), dz)))
+        )
+    }
+}
+ +
+

5. Optional: Change +Axes Dynamically

+

You could replace your rotationAxis system by letting +the user click to toggle which axes are active, then +generate quaternions for each active axis and multiply them +together.

+
+

If you want, I can rewrite your entire code snippet using +quaternions, so the cube rotates smoothly around multiple axes +and keeps your click-to-change-axis feature.

+

Do you want me to do that?

+

Absolutely! Let’s go through your quaternion-based 3D cube +code line by line. I’ll break it down into sections so it’s +easier to digest.

+
+

1. Canvas Setup

+
const game = document.getElementById("game");
+game.width = 400;
+game.height = 400;
+ +
const BACKGROUND = "#101010";
+const FOREGROUND = "#50FF50";
+const TEXT = "#FFFFFF";
+ +
const ctx = game.getContext("2d");
+ +
+

2. Cube Geometry

+
const vs = [ ... ];
+ +
const fs = [ ... ];
+ +
let dz = 1;
+let angle = 0;
+let activeAxes = ["x"];
+ +
+

3. Quaternion +Functions

+

a. Create a quaternion from an axis + angle

+
function quatFromAxisAngle(axis, angle) {
+    const half = angle / 2;
+    const s = Math.sin(half);
+    return [
+        Math.cos(half),   // w
+        axis.x * s,       // x
+        axis.y * s,       // y
+        axis.z * s        // z
+    ];
+}
+ +

b. Multiply two quaternions

+
function quatMultiply(q1, q2) { ... }
+ +

c. Rotate a vector by a quaternion

+
function quatRotateVector(q, v) { ... }
+ +
+

4. Drawing Functions

+

Clear canvas and draw text

+
function clear() {
+    ctx.fillStyle = BACKGROUND;
+    ctx.fillRect(0, 0, game.width, game.height);
+    ctx.fillStyle = TEXT;
+    ctx.font = "20px serif";
+    ctx.fillText(" *click me to change rotation axes", 10, game.height - 15);
+    ctx.fillText("current axes: " + activeAxes.join(","), 10, 20);
+}
+ +

Draw a point

+
function point({x, y}) {
+    const s = 20;
+    ctx.fillStyle = FOREGROUND;
+    ctx.fillRect(x - s/2, y - s/2, s, s);
+}
+ +

Draw a line

+
function line(p1, p2) { ... }
+ +

Convert 3D to screen coordinates

+
function screen(p) {
+    return {
+        x: (p.x + 1)/2 * game.width,
+        y: (1 -(p.y + 1)/2) * game.height
+    };
+}
+ +

Project 3D to 2D

+
function project({x, y, z}) {
+    return { x: x/z, y: y/z };
+}
+ +

Translate along Z

+
function translateZ({x,y,z}, dz) {
+    return {x, y, z: z + dz};
+}
+ +
+

5. Animation Loop

+
const FPS = 60;
+
+function frame() {
+    const dt = 1/FPS;
+    angle += Math.PI*dt;
+ +

Create combined quaternion

+
    let q = [1,0,0,0]; // identity quaternion
+    activeAxes.forEach(axis => {
+        let qAxis;
+        if (axis === "x") qAxis = quatFromAxisAngle({x:1,y:0,z:0}, angle);
+        if (axis === "y") qAxis = quatFromAxisAngle({x:0,y:1,z:0}, angle);
+        if (axis === "z") qAxis = quatFromAxisAngle({x:0,y:0,z:1}, angle);
+        q = quatMultiply(qAxis, q); // multiply in order
+    });
+ +

Draw the cube

+
    clear();
+    for (const f of fs) {
+        for (let i = 0; i < f.length; ++i) {
+            const a = vs[f[i]];
+            const b = vs[f[(i + 1)%f.length]];
+            line(
+                screen(project(translateZ(quatRotateVector(q, a), dz))),
+                screen(project(translateZ(quatRotateVector(q, b), dz)))
+            );
+        }
+    }
+ +

Schedule next frame

+
    setTimeout(frame, 1000/FPS);
+}
+setTimeout(frame, 1000/FPS);
+ +
+

6. Handle Clicks

+
game.onclick = () => {
+    const allAxes = [["x"], ["y"], ["z"], ["x","y"], ["y","z"], ["x","z"], ["x","y","z"]];
+    const index = allAxes.findIndex(a => JSON.stringify(a) === JSON.stringify(activeAxes));
+    activeAxes = allAxes[(index + 1) % allAxes.length];
+};
+ +
+

✅ Summary

+ +
+

If you want, I can also draw a diagram showing the quaternion +rotation concept visually — it really helps to understand how +it rotates the cube around multiple axes at once.

+

Do you want me to make that diagram?

+
+ + +
+

Great question — this is exactly the right point to slow down and +understand the math, because quaternions feel “magical” +until you see why each line exists.

+

I’ll go step by step:

+
+

1. What is a quaternion (in +graphics)?

+

A quaternion is a 4-component number:

+

[ q = (w, x, y, z)]

+

In 3D graphics, we use unit quaternions to represent +rotations.

+

A rotation by:

+ +

is encoded as:

+

[ ]

+

This is the axis–angle → quaternion formula.

+

Why half-angle? Because quaternion rotation comes from complex +numbers and Euler’s formula — doubling happens naturally in the +math.

+
+

2. quatFromAxisAngle

+
function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+

Math behind it

+

You’re directly implementing:

+

[ q = ((/2), (/2))]

+ +

Important rule

+

axis must be normalized (length = 1), +otherwise rotation will scale or skew.

+
+

3. Quaternion +multiplication (rotation composition)

+
function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+

Math formula

+

Given: [ q_1 = (w_1, _1), q_2 = (w_2, _2)]

+

Quaternion multiplication is:

+

[ ]

+

Expanded component-wise → exactly your code.

+

Why this matters

+ +

This avoids gimbal lock, unlike Euler angles.

+
+

4. Quaternion conjugate

+
function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+

Math meaning

+

For: [ q = (w, x, y, z)]

+

The conjugate is:

+

[ ]

+

Why this works

+

If (q) is a unit quaternion:

+

[ q^{-1} = q^*]

+

So conjugate = inverse rotation.

+

This is why cameras use conjugates — moving the world opposite to the +camera rotation.

+
+

5. Rotating a vector using +quaternions

+
function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+

The key formula

+

To rotate a vector ( ):

+
    +
  1. Convert it to a pure quaternion [ v_q = (0, v_x, +v_y, v_z)]

  2. +
  3. Apply: [ ]

  4. +
  5. Extract the vector part

  6. +
+

Why this works

+

Quaternion rotation encodes:

+ +

The sandwich product rotates without:

+ +
+

6. Why +quaternions are better than matrices (for rotation)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProblemEuler AnglesMatricesQuaternions
Gimbal lock❌❌✅
Smooth interpolation❌⚠️✅ (SLERP)
Compact✅❌✅
Numerical stability⚠️⚠️✅
+
+

7. Visual intuition

+

A rotation quaternion represents:

+

[ q = (/2) + (/2)(u_x i + u_y j + u_z k)]

+

This is the 3D analog of:

+

[ e^{i} = + i]

+
+

8. Best learning +resources (highly recommended)

+

Beginner-friendly

+ +

Graphics-oriented

+ +

Math reference

+ +
+

9. Mental model to keep

+
+

Quaternions don’t rotate vectors directly. They rotate +space itself around the vector.

+
+

Once that clicks, camera math suddenly makes sense.

+
+

If you want next:

+ +

Just tell me 👍

+ + diff --git a/docs/chatgpt-dump/quaternions.md b/docs/chatgpt-dump/quaternions.md new file mode 100644 index 0000000..c406140 --- /dev/null +++ b/docs/chatgpt-dump/quaternions.md @@ -0,0 +1,982 @@ +Absolutely! I can show you how to incorporate **quaternions** into your code to +allow smooth rotation around **multiple axes at once**, avoiding the limitations +of your current `rotate` function. + +Let’s go step by step. + +--- + +### 1. **Why Quaternions Here** + +Currently, you rotate around a single axis per frame: + +```js +rotate(a, rotationAxis, angle) +``` + +With quaternions, you can **combine rotations around multiple axes** seamlessly. For example, you could rotate simultaneously around X **and** Y. + +--- + +### 2. **Quaternion Basics** + +A quaternion can be represented as: + +[ +q = w + xi + yj + zk +] + +For 3D rotation: + +```js +q = [w, x, y, z] +``` + +* `w` = cos(angle/2) +* `x, y, z` = axis vector * sin(angle/2) + +To rotate a point `v`: + +1. Convert `v` to a quaternion `[0, vx, vy, vz]`. +2. Apply `v' = q * v * q⁻¹`. + +--- + +### 3. **Quaternion Utilities in JS** + +```js +function quatFromAxisAngle(axis, angle) { + const half = angle / 2 + const s = Math.sin(half) + return [ + Math.cos(half), // w + axis.x * s, // x + axis.y * s, // y + axis.z * s // z + ] +} + +function quatMultiply(q1, q2) { + const [w1,x1,y1,z1] = q1 + const [w2,x2,y2,z2] = q2 + return [ + w1*w2 - x1*x2 - y1*y2 - z1*z2, + w1*x2 + x1*w2 + y1*z2 - z1*y2, + w1*y2 - x1*z2 + y1*w2 + z1*x2, + w1*z2 + x1*y2 - y1*x2 + z1*w2 + ] +} + +function quatRotateVector(q, v) { + const [w,x,y,z] = q + const [vx,vy,vz] = [v.x,v.y,v.z] + + // Quaternion multiplication: v' = q * v_quat * q⁻¹ + const qv = [0, vx, vy, vz] + const qConj = [w, -x, -y, -z] + + const temp = quatMultiply(q, qv) + const result = quatMultiply(temp, qConj) + return { x: result[1], y: result[2], z: result[3] } +} +``` + +--- + +### 4. **Using Quaternions in Your Frame** + +Instead of your `rotate` function: + +```js +const angleX = angle; +const angleY = angle/2; // different speed for demo + +const qx = quatFromAxisAngle({x:1,y:0,z:0}, angleX) +const qy = quatFromAxisAngle({x:0,y:1,z:0}, angleY) + +const q = quatMultiply(qy, qx) // combined rotation + +for (const f of fs) { + for (let i = 0; i < f.length; ++i) { + const a = vs[f[i]] + const b = vs[f[(i + 1)%f.length]] + + line( + screen(project(translateZ(quatRotateVector(q, a), dz))), + screen(project(translateZ(quatRotateVector(q, b), dz))) + ) + } +} +``` + +* Now the cube rotates around **both X and Y axes simultaneously**. +* You can extend this to any combination of axes by multiplying more quaternions. + +--- + +### 5. **Optional: Change Axes Dynamically** + +You could replace your `rotationAxis` system by letting the user click to toggle **which axes are active**, then generate quaternions for each active axis and multiply them together. + +--- + +If you want, I can rewrite your **entire code snippet using quaternions**, so the cube rotates smoothly around multiple axes and keeps your click-to-change-axis feature. + +Do you want me to do that? + +Absolutely! Let’s go through your **quaternion-based 3D cube code** line by line. I’ll break it down into sections so it’s easier to digest. + +--- + +## **1. Canvas Setup** + +```js +const game = document.getElementById("game"); +game.width = 400; +game.height = 400; +``` + +* Gets the `` element with ID `game`. +* Sets the canvas dimensions to 400×400 pixels. + +```js +const BACKGROUND = "#101010"; +const FOREGROUND = "#50FF50"; +const TEXT = "#FFFFFF"; +``` + +* Defines colors for background, cube lines, and text. + +```js +const ctx = game.getContext("2d"); +``` + +* Gets the 2D drawing context so we can draw shapes on the canvas. + +--- + +## **2. Cube Geometry** + +```js +const vs = [ ... ]; +``` + +* `vs` = **vertices** of a cube (8 points). +* Each vertex has `x, y, z` coordinates in 3D space (values between -0.25 and 0.25). + +```js +const fs = [ ... ]; +``` + +* `fs` = **faces/edges** to connect vertices. +* Some are quads `[0,1,2,3]`, some are edges `[0,4]`. +* This tells the code **which points to draw lines between**. + +```js +let dz = 1; +let angle = 0; +let activeAxes = ["x"]; +``` + +* `dz`: distance to move the cube along Z (so it’s in front of the camera). +* `angle`: current rotation angle. +* `activeAxes`: which axes to rotate around (initially just `x`). + +--- + +## **3. Quaternion Functions** + +**a. Create a quaternion from an axis + angle** + +```js +function quatFromAxisAngle(axis, angle) { + const half = angle / 2; + const s = Math.sin(half); + return [ + Math.cos(half), // w + axis.x * s, // x + axis.y * s, // y + axis.z * s // z + ]; +} +``` + +* Converts **axis-angle rotation** into a quaternion `[w, x, y, z]`. +* `half` = half the rotation angle. +* `s = sin(half)` scales the axis. +* `w` = cos(half). + +**b. Multiply two quaternions** + +```js +function quatMultiply(q1, q2) { ... } +``` + +* Standard quaternion multiplication formula. +* Combines two rotations into one. **Order matters!** `q1*q2` rotates q2 first, then q1. + +**c. Rotate a vector by a quaternion** + +```js +function quatRotateVector(q, v) { ... } +``` + +* Rotates a 3D vector `v` using quaternion `q`. +* Steps: + + 1. Convert vector to quaternion `qv = [0, vx, vy, vz]`. + 2. Compute conjugate `qConj = [w, -x, -y, -z]`. + 3. Rotate: `v' = q * qv * q⁻¹`. + 4. Return new `x,y,z` of rotated vector. + +--- + +## **4. Drawing Functions** + +**Clear canvas and draw text** + +```js +function clear() { + ctx.fillStyle = BACKGROUND; + ctx.fillRect(0, 0, game.width, game.height); + ctx.fillStyle = TEXT; + ctx.font = "20px serif"; + ctx.fillText(" *click me to change rotation axes", 10, game.height - 15); + ctx.fillText("current axes: " + activeAxes.join(","), 10, 20); +} +``` + +* Clears the canvas with the background color. +* Draws instructions and current axes at the top/bottom. + +**Draw a point** + +```js +function point({x, y}) { + const s = 20; + ctx.fillStyle = FOREGROUND; + ctx.fillRect(x - s/2, y - s/2, s, s); +} +``` + +* Draws a small square at `(x, y)` on the screen. +* Not used in the final version, but useful for debugging vertices. + +**Draw a line** + +```js +function line(p1, p2) { ... } +``` + +* Draws a thick line from `p1` to `p2` in canvas coordinates. + +**Convert 3D to screen coordinates** + +```js +function screen(p) { + return { + x: (p.x + 1)/2 * game.width, + y: (1 -(p.y + 1)/2) * game.height + }; +} +``` + +* Maps normalized coordinates `[-1,1]` to screen coordinates `[0, width/height]`. +* Y-axis is flipped because canvas y=0 is top. + +**Project 3D to 2D** + +```js +function project({x, y, z}) { + return { x: x/z, y: y/z }; +} +``` + +* Simple **perspective projection**: divides x and y by z (makes things farther away appear smaller). + +**Translate along Z** + +```js +function translateZ({x,y,z}, dz) { + return {x, y, z: z + dz}; +} +``` + +* Moves the cube forward/backward so it appears in front of the camera. + +--- + +## **5. Animation Loop** + +```js +const FPS = 60; + +function frame() { + const dt = 1/FPS; + angle += Math.PI*dt; +``` + +* Updates rotation angle every frame based on FPS. + +**Create combined quaternion** + +```js + let q = [1,0,0,0]; // identity quaternion + activeAxes.forEach(axis => { + let qAxis; + if (axis === "x") qAxis = quatFromAxisAngle({x:1,y:0,z:0}, angle); + if (axis === "y") qAxis = quatFromAxisAngle({x:0,y:1,z:0}, angle); + if (axis === "z") qAxis = quatFromAxisAngle({x:0,y:0,z:1}, angle); + q = quatMultiply(qAxis, q); // multiply in order + }); +``` + +* Starts with **identity quaternion** (no rotation). +* For each active axis, creates a quaternion for rotation around that axis. +* Combines them by multiplying — this lets the cube rotate around multiple axes **simultaneously**. + +**Draw the cube** + +```js + clear(); + for (const f of fs) { + for (let i = 0; i < f.length; ++i) { + const a = vs[f[i]]; + const b = vs[f[(i + 1)%f.length]]; + line( + screen(project(translateZ(quatRotateVector(q, a), dz))), + screen(project(translateZ(quatRotateVector(q, b), dz))) + ); + } + } +``` + +* Clears the canvas. +* Loops through each face/edge `f`. +* Rotates each vertex using the combined quaternion. +* Translates it forward along Z. +* Projects 3D coordinates into 2D screen coordinates. +* Draws lines connecting vertices. + +**Schedule next frame** + +```js + setTimeout(frame, 1000/FPS); +} +setTimeout(frame, 1000/FPS); +``` + +* Calls `frame()` every 1/60th of a second to animate. + +--- + +## **6. Handle Clicks** + +```js +game.onclick = () => { + const allAxes = [["x"], ["y"], ["z"], ["x","y"], ["y","z"], ["x","z"], ["x","y","z"]]; + const index = allAxes.findIndex(a => JSON.stringify(a) === JSON.stringify(activeAxes)); + activeAxes = allAxes[(index + 1) % allAxes.length]; +}; +``` + +* On click, cycles through different **axis combinations**: x, y, z, xy, yz, xz, xyz. +* Updates `activeAxes` so the cube rotates differently next frame. + +--- + +### ✅ **Summary** + +* **Vertices + Faces**: Define cube geometry. +* **Quaternions**: Handle rotation around multiple axes safely. +* **Project & Screen Functions**: Convert 3D points to 2D canvas. +* **Animation Loop**: Updates rotation, applies quaternion rotation, and draws cube. +* **Click Handler**: Change which axes to rotate around. + +--- + +If you want, I can also **draw a diagram showing the quaternion rotation concept** visually — it really helps to understand how it rotates the cube around multiple axes at once. + +Do you want me to make that diagram? + +![diagram](https://nyquistdev.net/git/?p=basic-3d-example.git;a=blob_plain;f=docs/chatgpt-dump/quaternion-rotation.png;hb=HEAD) + +Great question — this is exactly the right point to slow down and understand the **math**, because quaternions feel “magical” until you see *why* each line exists. + +I’ll go step by step: + +--- + +# 1. What is a quaternion (in graphics)? + +A **quaternion** is a 4-component number: + +[ +q = (w, x, y, z) +] + +In 3D graphics, we use **unit quaternions** to represent **rotations**. + +A rotation by: + +* angle ( \theta ) +* around a **unit axis** ( \vec{u} = (u_x, u_y, u_z) ) + +is encoded as: + +[ +\boxed{ +q = \left( +\cos\frac{\theta}{2}, +; u_x \sin\frac{\theta}{2}, +; u_y \sin\frac{\theta}{2}, +; u_z \sin\frac{\theta}{2} +\right) +} +] + +This is the **axis–angle → quaternion** formula. + +Why half-angle? +Because quaternion rotation comes from complex numbers and Euler’s formula — doubling happens naturally in the math. + +--- + +# 2. `quatFromAxisAngle` + +```js +function quatFromAxisAngle(axis, angle) { + const h = angle / 2; + const s = Math.sin(h); + return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s]; +} +``` + +### Math behind it + +You’re directly implementing: + +[ +q = (\cos(\theta/2), \vec{u}\sin(\theta/2)) +] + +* `h = angle / 2` → half-angle +* `cos(h)` → scalar part (w) +* `axis * sin(h)` → vector part ((x,y,z)) + +### Important rule + +`axis` **must be normalized** (length = 1), otherwise rotation will scale or skew. + +--- + +# 3. Quaternion multiplication (rotation composition) + +```js +function quatMultiply(a, b) { + const [w1,x1,y1,z1] = a; + const [w2,x2,y2,z2] = b; + return [ + w1*w2 - x1*x2 - y1*y2 - z1*z2, + w1*x2 + x1*w2 + y1*z2 - z1*y2, + w1*y2 - x1*z2 + y1*w2 + z1*x2, + w1*z2 + x1*y2 - y1*x2 + z1*w2 + ]; +} +``` + +### Math formula + +Given: +[ +q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2) +] + +Quaternion multiplication is: + +[ +\boxed{ +q_1 q_2 = +\left( +w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,; +w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2 +\right) +} +] + +Expanded component-wise → exactly your code. + +### Why this matters + +* Quaternion multiplication **combines rotations** +* Order matters: + [ + q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}} + ] + +This avoids **gimbal lock**, unlike Euler angles. + +--- + +# 4. Quaternion conjugate + +```js +function quatConjugate(q) { + return [q[0], -q[1], -q[2], -q[3]]; +} +``` + +### Math meaning + +For: +[ +q = (w, x, y, z) +] + +The conjugate is: + +[ +\boxed{ +q^* = (w, -x, -y, -z) +} +] + +### Why this works + +If (q) is a **unit quaternion**: + +[ +q^{-1} = q^* +] + +So conjugate = **inverse rotation**. + +This is why cameras use conjugates — moving the world opposite to the camera rotation. + +--- + +# 5. Rotating a vector using quaternions + +```js +function quatRotateVector(q, v) { + const qv = [0, v.x, v.y, v.z]; + const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q)); + return { x: r[1], y: r[2], z: r[3] }; +} +``` + +### The key formula + +To rotate a vector ( \vec{v} ): + +1. Convert it to a **pure quaternion** + [ + v_q = (0, v_x, v_y, v_z) + ] + +2. Apply: + [ + \boxed{ + v' = q ; v_q ; q^{-1} + } + ] + +3. Extract the vector part + +### Why this works + +Quaternion rotation encodes: + +* axis +* angle +* orientation + +The sandwich product rotates without: + +* scaling +* skewing +* drifting + +--- + +# 6. Why quaternions are better than matrices (for rotation) + +| Problem | Euler Angles | Matrices | Quaternions | +| -------------------- | ------------ | -------- | ----------- | +| Gimbal lock | ❌ | ❌ | ✅ | +| Smooth interpolation | ❌ | ⚠️ | ✅ (SLERP) | +| Compact | ✅ | ❌ | ✅ | +| Numerical stability | ⚠️ | ⚠️ | ✅ | + +--- + +# 7. Visual intuition + +A rotation quaternion represents: + +[ +q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k) +] + +This is the 3D analog of: + +[ +e^{i\theta} = \cos\theta + i\sin\theta +] + +--- + +# 8. Best learning resources (highly recommended) + +### Beginner-friendly + +* **3Blue1Brown – Quaternions** + + * [https://www.youtube.com/watch?v=zjMuIxRvygQ](https://www.youtube.com/watch?v=zjMuIxRvygQ) +* **Visualizing Quaternions** + + * [https://eater.net/quaternions](https://eater.net/quaternions) + +### Graphics-oriented + +* **LearnOpenGL – Camera & Quaternions** + + * [https://learnopengl.com/Getting-started/Camera](https://learnopengl.com/Getting-started/Camera) +* **Scratchapixel – Quaternions** + + * [https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/quaternions) + +### Math reference + +* Wikipedia (surprisingly solid) + + * [https://en.wikipedia.org/wiki/Quaternion](https://en.wikipedia.org/wiki/Quaternion) + * [https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation](https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation) + +--- + +# 9. Mental model to keep + +> **Quaternions don’t rotate vectors directly. +> They rotate *space itself* around the vector.** + +Once that clicks, camera math suddenly makes sense. + +--- + +If you want next: + +* 🔄 quaternion → rotation matrix +* 🎥 why camera uses inverse rotation +* 🎯 SLERP (smooth rotation interpolation) +* 🧠 geometric intuition with diagrams + +Just tell me 👍 + +Absolutely — let’s unpack those sentences slowly and connect the symbols to intuition. + +--- + +## 1. “A quaternion is an expression of the form” + +[ +a + b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k} +] + +This means that **every quaternion can be written as a sum of four parts**: + +* one **real (scalar)** part +* three **imaginary (vector-like)** parts + +So a quaternion has **four real numbers** in it, not just two like a complex number. + +--- + +## 2. The coefficients (a, b, c, d) + +> “where (a, b, c, d) are real numbers” + +* These are just ordinary real numbers (like (2), (-1.3), (\pi), etc.). +* They tell you **how much** of each basis element the quaternion contains. + +Think of them as coordinates. + +--- + +## 3. The symbols (\mathbf{i}, \mathbf{j}, \mathbf{k}) + +> “and (\mathbf{i}, \mathbf{j}, \mathbf{k}) are symbols that can be interpreted as unit-vectors pointing along the three spatial axes.” + +These symbols are **not numbers** in the usual sense. They are **basis elements** that behave in special ways. + +### Interpretation: + +* (\mathbf{i}) → x-axis direction +* (\mathbf{j}) → y-axis direction +* (\mathbf{k}) → z-axis direction + +So: +[ +b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k} +] +looks exactly like a 3D vector: +[ +(b, c, d) +] + +--- + +## 4. Scalar part vs vector part + +The quaternion naturally splits into two pieces: + +[ +\underbrace{a}*{\text{scalar part}} +\quad + \quad +\underbrace{(b,\mathbf{i} + c,\mathbf{j} + d,\mathbf{k})}*{\text{vector part}} +] + +This matches the earlier notation: +[ +\mathbf{q} = (r, \vec{v}) +] +where: + +* (r = a) +* (\vec{v} = (b, c, d)) + +--- + +## 5. How this extends complex numbers + +Complex numbers look like: +[ +a + b,\mathbf{i} +] + +Quaternions **extend** this idea: + +* one real axis +* three imaginary axes + +So instead of a 2D number system, quaternions live in **4D**. + +--- + +## 6. Important difference from vectors + +Although (\mathbf{i}, \mathbf{j}, \mathbf{k}) *look* like unit vectors, they are actually **algebraic objects with multiplication rules**: + +[ +\mathbf{i}^2 = \mathbf{j}^2 = \mathbf{k}^2 = \mathbf{i}\mathbf{j}\mathbf{k} = -1 +] + +and multiplication is **not commutative**: +[ +\mathbf{i}\mathbf{j} = \mathbf{k}, \quad +\mathbf{j}\mathbf{i} = -\mathbf{k} +] + +This is why quaternions are so powerful for rotations. + +--- + +## 7. Geometric intuition + +You can think of a quaternion as: + +* a **real number** (a), plus +* a **3D direction and magnitude** ((b, c, d)) + +When used for rotations: + +* (a) relates to the rotation angle +* ((b, c, d)) relates to the rotation axis + +--- + +## 8. Plain-English summary + +> A quaternion is built from one real number and three imaginary components. The imaginary parts behave like directions in 3D space and follow special multiplication rules. Together, they form a 4-dimensional number system that can represent 3D rotations cleanly and efficiently. + +If you’d like, I can next: + +* show how quaternion multiplication works geometrically +* explain why quaternions avoid gimbal lock +* relate quaternions directly to rotation matrices + +Great question — this is where quaternions really become *geometric*, not just algebraic. + +I’ll build this up step by step, from intuition to the final geometric meaning. + +--- + +## 1. Unit quaternions represent rotations + +A **unit quaternion** (length = 1) represents a 3D rotation. + +It is written as: +[ +q = \cos\frac{\theta}{2} + \hat{u},\sin\frac{\theta}{2} +] +where: + +* (\theta) = rotation angle +* (\hat{u} = u_x\mathbf{i} + u_y\mathbf{j} + u_z\mathbf{k}) is a **unit vector** (rotation axis) + +This already encodes geometry: + +* **axis** → direction of the vector part +* **angle** → stored in the sine/cosine terms + +--- + +## 2. Rotating a vector using a quaternion + +A 3D vector (\vec{v}) is treated as a **pure quaternion**: +[ +\vec{v} ;\longrightarrow; 0 + v_x\mathbf{i} + v_y\mathbf{j} + v_z\mathbf{k} +] + +The rotated vector is: +[ +\vec{v},' = q,\vec{v},q^{-1} +] + +This is not arbitrary — this *is* the geometric action of rotation. + +--- + +## 3. What quaternion multiplication does geometrically + +### Key idea: + +> **Quaternion multiplication composes rotations.** + +If: + +* (q_1) rotates by (\theta_1) around axis (\hat{u}_1) +* (q_2) rotates by (\theta_2) around axis (\hat{u}_2) + +Then: +[ +q = q_2 q_1 +] +represents: + +> “First rotate by (q_1), then rotate by (q_2)” + +This is exactly how matrix multiplication works for rotations. + +--- + +## 4. Why order matters (non-commutativity) + +Quaternion multiplication is **not commutative**: +[ +q_1 q_2 \neq q_2 q_1 +] + +Geometrically: + +* Rotating around **x then y** is not the same as **y then x** + +This matches real-world 3D rotations. + +--- + +## 5. Vector–scalar decomposition of multiplication + +Let: +[ +q_1 = (r_1, \vec{v}_1), \quad +q_2 = (r_2, \vec{v}_2) +] + +Their product is: +[ +q_2 q_1 = +\big( +r_2 r_1 - \vec{v}_2 \cdot \vec{v}_1,; +r_2 \vec{v}_1 + r_1 \vec{v}_2 + \vec{v}_2 \times \vec{v}_1 +\big) +] + +### Geometric meaning of each term: + +* **Dot product** → angle alignment +* **Cross product** → axis perpendicular to both rotations +* **Linear terms** → mixing scalar and vector parts + +So quaternion multiplication naturally combines: + +* angle information (dot) +* axis orientation (cross) + +--- + +## 6. Visual intuition + +Think of a unit quaternion as: + +* a point on a **4D unit sphere** +* encoding a rotation in 3D space + +Multiplying two unit quaternions: + +* moves you along that sphere +* in a way that corresponds exactly to doing one rotation after another + +--- + +## 7. Why the half-angle appears + +You might wonder: + +> Why (\theta/2), not (\theta)? + +Geometrically: + +* Rotations form a curved space (SO(3)) +* Unit quaternions live in a simpler space (SU(2)) that *double-covers* rotations +* The half-angle makes multiplication correspond cleanly to rotation composition + +That’s why: +[ +q \text{ and } -q +] +represent the **same rotation**. + +--- + +## 8. Simple concrete example + +Rotate 90° about the z-axis: +[ +q = \cos 45^\circ + \mathbf{k}\sin 45^\circ +] + +Apply it to (\mathbf{i}) (the x-axis): +[ +\mathbf{i}' = q,\mathbf{i},q^{-1} = \mathbf{j} +] + +Geometrically: + +* x-axis rotates into y-axis +* exactly what you expect + +--- + +## 9. Plain-English summary + +> Quaternion multiplication is not “multiplying numbers.” +> It is **combining rotations in 3D space**. +> The algebra mirrors the geometry: dot products handle angles, cross products handle axes, and order encodes rotation order. + +If you want, next I can: + +* derive the rotation formula step-by-step +* show the link to rotation matrices +* explain the double-cover and why (q) and (-q) are the same rotation + diff --git a/docs/chatgpt-dump/rendered.html b/docs/chatgpt-dump/rendered.html new file mode 100644 index 0000000..a9595b6 --- /dev/null +++ b/docs/chatgpt-dump/rendered.html @@ -0,0 +1,468 @@ + + + + + + + +Untitled + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/example.html b/example.html new file mode 100644 index 0000000..0883092 --- /dev/null +++ b/example.html @@ -0,0 +1,78 @@ + + + + + + + +Canvas + Code + + + + +
+ +
+       
+    
+ +
+ + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..f2f8a47 --- /dev/null +++ b/index.html @@ -0,0 +1,9 @@ + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..8bfa577 --- /dev/null +++ b/index.js @@ -0,0 +1,186 @@ +const canvas = document.getElementById("game"); +const ctx = canvas.getContext("2d") + +const BACKGROUND = "#000000"; +const FOREGROUND = "#00FF00"; +const FPS = 60; + +// let debug = true; +let debug = false; + +let debugColors = [ + "#FF0000", + "#00FF00", + "#0000FF", +] + +const highContrastColors = [ + "#FFFF00", // 1. Bright Yellow + "#FF00FF", // 2. Magenta + "#00FFFF", // 3. Cyan / Aqua + "#FFA500", // 4. Pure Orange + "#800080", // 5. Deep Purple + "#008080", // 6. Teal + "#A52A2A", // 7. Brown + "#800000", // 8. Maroon + "#008000", // 9. Forest Green + "#000080", // 10. Navy Blue + "#808000", // 11. Olive + "#C71585", // 12. Hot Pink + "#FFC0CB", // 13. Light Pink + "#7FFFD4", // 14. Aquamarine + "#FFE4E1", // 15. Misty Rose + "#F0E68C", // 16. Khaki + "#E6E6FA", // 17. Lavender + "#B0C4DE", // 18. Light Steel Blue + "#FF4500", // 19. Red-Orange + "#1E90FF", // 20. Dodger Blue + "#FFD700", // 21. Gold + "#9370DB", // 22. Medium Purple + "#8B008B", // 23. Dark Magenta + "#556B2F", // 24. Dark Olive Green + "#FF8C00", // 25. Dark Orange + "#48D1CC", // 26. Medium Turquoise + "#32CD32", // 27. Bright Green + "#2F4F4F", // 28. Dark Slate Gray + "#98FF98", // 29. Mint Green + "#FF7F50", // 30. Coral + "#4B0082", // 31. Indigo + "#D2B48C", // 32. Tan / Sand + "#FF7F55", // 30. Coral +]; + +canvas.width = 800; +canvas.height = 800; + +function clear() { + ctx.fillStyle = BACKGROUND; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + +function line(p1, p2) { + ctx.lineWidth = 8; + if (!debug) ctx.strokeStyle = FOREGROUND; + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); +} + +function getMagnitude(a, b) { + let x = (b.x - a.x); + let y = (b.y - a.y); + return Math.sqrt(x * x + y * y); +} + +function interpolate(a, b, y) { + let startingPos = a.x; + let incX = (b.x - a.x) / (b.y - a.y); + let numberOfyIncs = y - a.y; + return startingPos + incX * numberOfyIncs; +} + +function drawTriangle(coords, color) { + coords.sort((a,b) => a.y - b.y); + + let blueisSteeperThanRed = + (coords[2].y - coords[0].y) * (coords[1].x - coords[0].x) > + (coords[1].y - coords[0].y) * (coords[2].x - coords[0].x) + + for (let y = coords[0].y; y <= coords[2].y; ++y) { + + let x1 = interpolate(coords[0], coords[2], y); + + let x2 = (y < coords[1].y) ? + interpolate(coords[0], coords[1], y) : + interpolate(coords[1], coords[2], y); + + let fill = (i, y) => { + ctx.fillStyle = color; + ctx.fillRect(i, y, 2, 2); + } + + if (blueisSteeperThanRed) { + for (let i = x1; i < x2; ++i) fill(i, y); + } else { + for (let i = x1; i > x2; --i) fill(i, y); + + } + } + + if (debug) { + for (const i in coords) { + ctx.strokeStyle = debugColors[i]; + let a = coords[i]; + let b = coords[ (i + 1) % coords.length]; + line(a, b); + } + + } + + +} + + +clear(); +let moveX = 0; + +let triangleCoords = [ + // --- CENTER CRADLE (Core focal points) --- + [{x: 400, y: 400}, {x: 300, y: 300}, {x: 500, y: 250}], // High acute top wedge + [{x: 400, y: 400}, {x: 500, y: 250}, {x: 550, y: 420}], // Narrow vertical sliver + [{x: 400, y: 400}, {x: 550, y: 420}, {x: 480, y: 550}], // Rotated downward blade + [{x: 400, y: 400}, {x: 480, y: 550}, {x: 280, y: 480}], // Broad foundational center piece + [{x: 400, y: 400}, {x: 280, y: 480}, {x: 300, y: 300}], // Left-leaning internal wedge + + // --- NORTH FLANK (Radiating to the top edge: y = 0) --- + [{x: 300, y: 300}, {x: 0, y: 0}, {x: 250, y: 0}], + [{x: 300, y: 300}, {x: 250, y: 0}, {x: 500, y: 0}], + [{x: 300, y: 300}, {x: 500, y: 0}, {x: 500, y: 250}], // Massive sweeping North-East shard + [{x: 500, y: 250}, {x: 500, y: 0}, {x: 700, y: 0}], + [{x: 500, y: 250}, {x: 700, y: 0}, {x: 800, y: 150}], // Long, sharp slope + + // --- EAST FLANK (Radiating to the right edge: x = 800) --- + [{x: 500, y: 250}, {x: 800, y: 150}, {x: 550, y: 420}], + [{x: 550, y: 420}, {x: 800, y: 150}, {x: 800, y: 450}], // Tight accent block + [{x: 550, y: 420}, {x: 800, y: 450}, {x: 800, y: 600}], + [{x: 550, y: 420}, {x: 800, y: 600}, {x: 480, y: 550}], // Giant horizontal slice + + // --- SOUTH FLANK (Radiating to the bottom edge: y = 800) --- + [{x: 480, y: 550}, {x: 800, y: 600}, {x: 800, y: 800}], // Clears the bottom right corner + [{x: 480, y: 550}, {x: 800, y: 800}, {x: 550, y: 800}], + [{x: 480, y: 550}, {x: 550, y: 800}, {x: 350, y: 800}], // Long vertical grounding anchor + [{x: 480, y: 550}, {x: 350, y: 800}, {x: 150, y: 800}], + [{x: 480, y: 550}, {x: 150, y: 800}, {x: 280, y: 480}], // Sweeping South-West transitional floor + + // --- WEST FLANK (Radiating to the left edge: x = 0) --- + [{x: 280, y: 480}, {x: 150, y: 800}, {x: 0, y: 800}], // Clears the bottom left corner + [{x: 280, y: 480}, {x: 0, y: 800}, {x: 0, y: 500}], + [{x: 280, y: 480}, {x: 0, y: 500}, {x: 0, y: 300}], // Tall vertical perimeter guard + [{x: 280, y: 480}, {x: 0, y: 300}, {x: 300, y: 300}], // Steep internal left block + + // --- NORTH-WEST FILLER ZONE (Completing upper left frame gaps) --- + [{x: 300, y: 300}, {x: 0, y: 300}, {x: 0, y: 120}], + [{x: 300, y: 300}, {x: 0, y: 120}, {x: 0, y: 0}], + + // --- OUTER SUBDIVISIONS (Adding complexity to the borders for visual contrast) --- + [{x: 700, y: 0}, {x: 800, y: 0}, {x: 800, y: 150}], // Top right micro-corner + [{x: 550, y: 800}, {x: 680, y: 800}, {x: 480, y: 550}], // Tiny baseline shard A + [{x: 680, y: 800}, {x: 800, y: 800}, {x: 480, y: 550}], // Tiny baseline shard B + [{x: 350, y: 800}, {x: 250, y: 800}, {x: 480, y: 550}], // Tiny baseline shard C + [{x: 250, y: 800}, {x: 150, y: 800}, {x: 480, y: 550}], // Tiny baseline shard D + [{x: 0, y: 500}, {x: 0, y: 650}, {x: 280, y: 480}], // Left margin splitter + [{x: 0, y: 650}, {x: 0, y: 800}, {x: 280, y: 480}], // Left floor splitter + [{x: 0, y: 300}, {x: 0, y: 200}, {x: 300, y: 300}], // Left ceiling splitter + [{x: 0, y: 200}, {x: 0, y: 0}, {x: 300, y: 300}] // Left corner lock +]; + +// drawTriangle([{x: moveX + 200, y: 200}, {x: moveX + 600, y: 310},{x: moveX + 450, y: 400}]); +// drawTriangle([{x: 120, y: 200}, {x:200, y: 310},{x: 450, y: 400}], "#FF0000"); +// drawTriangle([{x: 200, y: 200}, {x:600, y: 230},{x: 250, y: 300}]); + + +for (const i in triangleCoords) { + drawTriangle(triangleCoords[i], highContrastColors[i % highContrastColors.length]); + await new Promise(r => setTimeout(r, 2000)); +} diff --git a/look-at(getViewMatrix).png b/look-at(getViewMatrix).png new file mode 100644 index 0000000..81abb7d Binary files /dev/null and b/look-at(getViewMatrix).png differ diff --git a/notes.adoc b/notes.adoc new file mode 100644 index 0000000..a6ea044 --- /dev/null +++ b/notes.adoc @@ -0,0 +1,6 @@ +== Links +* https://www.youtube.com/watch?v=U0_ONQQ5ZNM + +* https://learnopengl.com/Getting-started/Camera[Learn OpenGL | Camera] + ** You can use this link to understand the getViewMatrix (look at matrix) + function better diff --git a/reference.js b/reference.js new file mode 100644 index 0000000..e25705f --- /dev/null +++ b/reference.js @@ -0,0 +1,264 @@ + +/* ===================== SETUP ===================== */ +const game = document.getElementById("game"); +game.width = 800; +game.height = 800; +const ctx = game.getContext("2d"); + +const BACKGROUND = "#101010"; +const FOREGROUND = "#50FF50"; +const TEXT = "#FFFFFF"; + +const FPS = 60; +const NEAR = 0.1; + +/* ===================== GEOMETRY ===================== */ +const vertices = [ + {x: 0.25, y: 0.25, z: 0.25}, + {x: -0.25, y: 0.25, z: 0.25}, + {x: -0.25, y: -0.25, z: 0.25}, + {x: 0.25, y: -0.25, z: 0.25}, + {x: 0.25, y: 0.25, z: -0.25}, + {x: -0.25, y: 0.25, z: -0.25}, + {x: -0.25, y: -0.25, z: -0.25}, + {x: 0.25, y: -0.25, z: -0.25}, +]; + +const edges = [ + [0,1,2,3], + [4,5,6,7], + [0,4],[1,5],[2,6],[3,7] +]; + +/* ===================== OBJECT ===================== */ +let objectPos = { x: 0, y: 0, z: 2 }; +let angle = 0; + +const AXIS_MODES = [ + ["x"], ["y"], ["z"], + ["x","y"], ["y","z"], ["x","z"], + ["x","y","z"] +]; +let axisModeIndex = AXIS_MODES.length-1; +let activeAxes = AXIS_MODES[axisModeIndex]; + +/* ===================== CAMERA ===================== */ +let camera = { + position: { x: 0, y: 0, z: 0 }, + rotation: [1,0,0,0], + speed: 0.05, + sensitivity: 0.002 +}; + +/* ===================== INPUT ===================== */ +const keys = {}; +window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true); +window.addEventListener("keyup", e => keys[e.key.toLowerCase()] = false); + +/* ===================== QUATERNIONS ===================== */ +function quatFromAxisAngle(axis, angle) { + const h = angle / 2; + const s = Math.sin(h); + return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s]; +} + +function quatMultiply(a, b) { + const [w1,x1,y1,z1] = a; + const [w2,x2,y2,z2] = b; + return [ + w1*w2 - x1*x2 - y1*y2 - z1*z2, + w1*x2 + x1*w2 + y1*z2 - z1*y2, + w1*y2 - x1*z2 + y1*w2 + z1*x2, + w1*z2 + x1*y2 - y1*x2 + z1*w2 + ]; +} + +function quatConjugate(q) { + return [q[0], -q[1], -q[2], -q[3]]; +} + +function quatRotateVector(q, v) { + // whenever w is zero then we are on the unit sphere + // unit quanternion + const qv = [0, v.x, v.y, v.z]; + + // the conjugate is equal to the inverse quanternion if q is a unit-norm + // quanternion. in this case it is a unit-norm quanternion + const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q)); + return { x: r[1], y: r[2], z: r[3] }; +} + +/* ===================== CAMERA VECTORS ===================== */ +function cameraForward() { + return quatRotateVector(camera.rotation, {x:0,y:0,z:1}); +} +function cameraRight() { + return quatRotateVector(camera.rotation, {x:1,y:0,z:0}); +} +function cameraUp() { + return quatRotateVector(camera.rotation, {x:0,y:1,z:0}); +} + +function applyCamera(p) { + const v = { + x: p.x - camera.position.x, + y: p.y - camera.position.y, + z: p.z - camera.position.z + }; + // we use the conjugate here because the camera should move everything in the + // opposite direction that we are moving + return quatRotateVector(quatConjugate(camera.rotation), v); +} + +/* ===================== PROJECTION ===================== */ +function project(p) { + return { x: p.x / p.z, y: p.y / p.z }; +} +function screen(p) { + return { + x: (p.x + 1) * 0.5 * game.width, + y: (1 - (p.y + 1) * 0.5) * game.height + }; +} + +/* ===================== DRAW ===================== */ +function clear() { + ctx.fillStyle = BACKGROUND; + ctx.fillRect(0,0,game.width,game.height); + + ctx.fillStyle = TEXT; + ctx.font = "14px monospace"; + ctx.fillText("WASD: move | QE: up/down", 10, 18); + ctx.fillText("Mouse: look | Click canvas to lock", 10, 36); + ctx.fillText("Click canvas again: change cube axes", 10, 54); + ctx.fillText("Cube axes: " + activeAxes.join(","), 10, 72); +} + +function line(a,b) { + ctx.strokeStyle = FOREGROUND; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(a.x,a.y); + ctx.lineTo(b.x,b.y); + ctx.stroke(); +} + +/* ===================== LOOP ===================== */ +function frame() { + angle += Math.PI / FPS; + + // --- cube rotation quaternion --- + + // This is the center of the cube. we are going to rotate around the center + // of the cube. + let q = [1,0,0,0]; + + // First, have the correct unit quanternion + for (const ax of activeAxes) { + // quatFromAxixAngle are unit quanternions + // as we keep pulling the quanternion in the axis direction further and + // further, you keep rotating the quanternion + + // we will use the right hand rule to figure out what is going on. + + // the left side of the multiplication is the function of the right + + // REMEMBER + // These axis angles are part of the fourth demension and won't be + // visible + + // x is i. so we are going in the positive i direction + // --------------------------------------------------- + // the jk circle (The part we see) rotates from j to kk + if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q); + + // y is j. So we are going in the positive j direction + // --------------------------------------------------- + // the ij circle (part we see) rotates from -i to k + if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q); + + // z is k. So we are going in the positve k direction + // --------------------------------------------------- + // The ij circle rotates from j to -i + if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q); + } + + // --- camera movement --- + if (keys["w"]) { + const f = cameraForward(); + camera.position.x += f.x * camera.speed; + camera.position.y += f.y * camera.speed; + camera.position.z += f.z * camera.speed; + } + if (keys["s"]) { + const f = cameraForward(); + camera.position.x -= f.x * camera.speed; + camera.position.y -= f.y * camera.speed; + camera.position.z -= f.z * camera.speed; + } + if (keys["a"]) { + const r = cameraRight(); + camera.position.x -= r.x * camera.speed; + camera.position.y -= r.y * camera.speed; + camera.position.z -= r.z * camera.speed; + } + if (keys["d"]) { + const r = cameraRight(); + camera.position.x += r.x * camera.speed; + camera.position.y += r.y * camera.speed; + camera.position.z += r.z * camera.speed; + } + if (keys["q"]) { + const u = cameraUp(); + camera.position.x -= u.x * camera.speed; + camera.position.y -= u.y * camera.speed; + camera.position.z -= u.z * camera.speed; + } + if (keys["e"]) { + const u = cameraUp(); + camera.position.x += u.x * camera.speed; + camera.position.y += u.y * camera.speed; + camera.position.z += u.z * camera.speed; + } + + clear(); + + for (const e of edges) { + for (let i=0;i { + game.requestPointerLock(); + axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length; + activeAxes = AXIS_MODES[axisModeIndex]; +}; + +window.addEventListener("mousemove", e => { + if (document.pointerLockElement !== game) return; + + const yaw = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity); + const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity); + + camera.rotation = quatMultiply(yaw, camera.rotation); + camera.rotation = quatMultiply(camera.rotation, pitch); +}); diff --git a/rendered.html b/rendered.html new file mode 100644 index 0000000..45d3678 --- /dev/null +++ b/rendered.html @@ -0,0 +1,1538 @@ + + + + + + + +Basic 3D Example + + + + + + + + + + + + + + + + + + + +
+
+
+
+

click here for an interactive example

+
+
+

A good explaination of how 3D works.

+
+
+ + + + + +
+ + +This started off as code from Tsoding, I used chatgpt to change + specific things, and also made some of my own changes. This is a learning + tool! not something I wrote off the top of my head +
+
+
+
+
+

Graphics Pipeline

+
+
+
+
vertex
+ -> rotate cube
+ -> translate cube (object position)
+ -> apply camera (inverse transform)
+ -> project
+ -> screen
+
+
+
+ + + + + +
+ + +Some parts of these pipeline may be left out if that part of the pipeline + is not needed +
+
+
+
+
+

Basic Projection Formula

+
+
+

Projection formula for a vector \([x,y,z\)]:

+
+
+
+\[ +\begin{aligned} +x' &= x/z \\ +y' &= y/z +\end{aligned} +\] +
+
+
+
+
+

Vertex Rotation Formulas

+
+
+

click here for an interactive example

+
+
+

Rotate vector formula for x-axis (y-z plane):

+
+
+
+\[ +\begin{aligned} +x' &= x \\ +y' &= y\cos\theta - z\sin\theta \\ +z' &= y\sin\theta + z\cos\theta +\end{aligned} +\] +
+
+
+

Rotate vector formula for y-axis (x-z plane):

+
+
+
+\[ +\begin{aligned} +x' &= x\cos\theta - z\sin\theta \\ +y' &= y \\ +z' &= x\sin\theta + z\cos\theta +\end{aligned} \] +
+
+
+

Rotate vector formula for z-axis (x-y plane):

+
+
+
+\[ +\begin{aligned} +x' &= x\cos\theta - y\sin\theta \\ +y' &= x\sin\theta + y\cos\theta \\ +z' &= z +\end{aligned} +\] +
+
+
+
+
+

2. Single-axis rotation matrices (Euler Angles)

+
+
+

Rotation about X-axis (roll)

+
+
+\[\boxed{ +R_x(\alpha) = +\begin{bmatrix} +1 & 0 & 0 \\ +0 & \cos\alpha & -\sin\alpha \\ +0 & \sin\alpha & \cos\alpha +\end{bmatrix} +}\] +
+
+
+
+

Rotation about Y-axis (pitch)

+
+
+\[\boxed{ +R_y(\beta) = +\begin{bmatrix} +\cos\beta & 0 & \sin\beta \\ +0 & 1 & 0 \\ +-\sin\beta & 0 & \cos\beta +\end{bmatrix} +}\] +
+
+
+
+

Rotation about Z-axis (yaw)

+
+
+\[\boxed{ +R_z(\gamma) = +\begin{bmatrix} +\cos\gamma & -\sin\gamma & 0 \\ +\sin\gamma & \cos\gamma & 0 \\ +0 & 0 & 1 +\end{bmatrix} +}\] +
+
+
+

Euler angles will always have gimbal lock. You can switch which axis has gimbal +lock by switching the order. Most application use quaternions instead of eular +angles.

+
+
+
+
+
+

Quaternions

+
+
+

click here for an interactive example

+
+
+

Quaternions allow you to rotate smoothly around multiple axes without gimbal +lock. Chat-gpt rewrote the code to use quaternions. Here are some of the +replys about quaternions.

+
+
+

1. What is a quaternion (in graphics)?

+
+

A quaternion is a 4-component number:

+
+
+
+\[q = (w, x, y, z)\] +
+
+
+

In 3D graphics, we use unit quaternions to represent rotations.

+
+
+

A rotation by:

+
+
+
    +
  • +

    angle \(\theta\)

    +
  • +
  • +

    around a unit axis \(\vec{u} = (u_x, u_y, u_z)\)

    +
  • +
+
+
+

is encoded as:

+
+
+
+\[\boxed{ +q = \left( +\cos\frac{\theta}{2}, +\; u_x \sin\frac{\theta}{2}, +\; u_y \sin\frac{\theta}{2}, +\; u_z \sin\frac{\theta}{2} +\right) +}\] +
+
+
+

This is the axis–angle → quaternion formula.

+
+
+

Why half-angle?

+
+

Because quaternion rotation comes from complex numbers and Euler’s formula — +doubling happens naturally in the math.

+
+
+
+
+

2. quatFromAxisAngle

+
+
+
function quatFromAxisAngle(axis, angle) {
+    const h = angle / 2;
+    const s = Math.sin(h);
+    return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
+}
+
+
+
+

You’re directly implementing:

+
+
+
+\[q = (\cos(\theta/2), \vec{u}\sin(\theta/2))\] +
+
+
+
    +
  • +

    \(h = angle / 2\) → half-angle

    +
  • +
  • +

    \(cos(h)\) → scalar part (w)

    +
  • +
  • +

    \(axis * sin(h)\) → vector part \((x,y,z)\)

    +
  • +
+
+
+ + + + + +
+ + +axis must be normalized (length = 1), otherwise rotation will + scale or skew. +
+
+
+
+

3. Quaternion multiplication (rotation composition)

+
+
+
function quatMultiply(a, b) {
+    const [w1,x1,y1,z1] = a;
+    const [w2,x2,y2,z2] = b;
+    return [
+        w1*w2 - x1*x2 - y1*y2 - z1*z2,
+        w1*x2 + x1*w2 + y1*z2 - z1*y2,
+        w1*y2 - x1*z2 + y1*w2 + z1*x2,
+        w1*z2 + x1*y2 - y1*x2 + z1*w2
+    ];
+}
+
+
+
+

formula:

+
+
+
+\[q_1 = (w_1, \vec{v}_1), \quad q_2 = (w_2, \vec{v}_2)\] +
+
+
+

Quaternion multiplication is:

+
+
+
+\[\boxed{ +q_1 q_2 = +\left( +w_1 w_2 - \vec{v}_1 \cdot \vec{v}_2,\; +w_1 \vec{v}_2 + w_2 \vec{v}_1 + \vec{v}_1 \times \vec{v}_2 +\right) +}\] +
+
+
+

Expanded component-wise → exactly your code.

+
+
+

Why this matters

+
+
    +
  • +

    Quaternion multiplication combines rotations

    +
  • +
  • +

    Order matters:

    +
  • +
+
+
+
+\[q_{\text{final}} = q_{\text{new}} \cdot q_{\text{current}}\] +
+
+
+

This avoids gimbal lock, unlike Euler angles.

+
+
+
+
+

4. Quaternion conjugate

+
+
+
function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
+}
+
+
+
+

For:

+
+
+
+\[q = (w, x, y, z)\] +
+
+
+

The conjugate is:

+
+
+
+\[\boxed{ +q^* = (w, -x, -y, -z) +}\] +
+
+
+
+

Why this works

+
+

If (q) is a unit quaternion:

+
+
+
+\[q^{-1} = q^*\] +
+
+
+

So conjugate = inverse rotation.

+
+
+

This is why cameras use conjugates — moving the world opposite to the camera +rotation.

+
+
+
+

5. Rotating a vector using quaternions

+
+
+
function quatRotateVector(q, v) {
+    const qv = [0, v.x, v.y, v.z];
+    const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
+    return { x: r[1], y: r[2], z: r[3] };
+}
+
+
+
+

The key formula

+
+
+

To rotate a vector \(\vec{v}\):

+
+
+
    +
  1. +

    Convert it to a pure quaternion

    +
  2. +
+
+
+
+\[v_q = (0, v_x, v_y, v_z)\] +
+
+
+
    +
  1. +

    Apply:

    +
  2. +
+
+
+
+\[\boxed{ +v' = q \; v_q \; q^{-1} +}\] +
+
+
+
    +
  1. +

    Extract the vector part

    +
  2. +
+
+
+

Why this works

+
+

Quaternion rotation encodes:

+
+
+
    +
  • +

    axis

    +
  • +
  • +

    angle

    +
  • +
  • +

    orientation

    +
  • +
+
+
+

The sandwich product rotates without:

+
+
+
    +
  • +

    scaling

    +
  • +
  • +

    skewing

    +
  • +
  • +

    drifting

    +
  • +
+
+
+
+
+

Visual intuition

+
+

A rotation quaternion represents:

+
+
+
+\[q = \cos(\theta/2) + \sin(\theta/2)(u_x i + u_y j + u_z k)\] +
+
+
+

This is the 3D analog of:

+
+
+
+\[e^{i\theta} = \cos\theta + i\sin\theta\] +
+
+
+
+
+
+

Camera

+
+
+

chat-gpt replys about camera.

+
+
+
+
+

Simple clickable object

+
+
+

click here for an interactive example

+
+
+

This does not account for the rotation of the object but it does check to see if +your mouse is within the rectangular bounds of the mesh.

+
+
+
    +
  1. +

    finding the min and max values for x and y of the mesh:

    +
  2. +
+
+
+
+
function computeBounds(mesh) {
+    let min = {x: Infinity, y: Infinity}
+    let max = {x: -Infinity, y: -Infinity}
+
+    for (const v of mesh) {
+        min.x = Math.min(min.x, v.x)
+        min.y = Math.min(min.y, v.y)
+
+        max.x = Math.max(max.x, v.x)
+        max.y = Math.max(max.y, v.y)
+    }
+    return {min, max}
+}
+
+
+
+
    +
  1. +

    Define the box corners of the rect for the mesh

    +
  2. +
+
+
+
+
function boundingBoxCorners({min, max}) {
+    return {
+        bottom: {left: {x: min.x, y: max.y}, right: {x: max.x, y: max.y}},
+        top: {left: {x: min.x, y: min.y}, right: {x: max.x, y: min.y}},
+
+    }
+}
+
+
+
+
    +
  1. +

    Check to see if the mouse is in the rect in the game loop. If it is, and the +mouse button is down, then you can move the object

    +
  2. +
+
+
+
+
inObject =  (
+    mousePos.x > rect.top.left.x
+    &&
+    mousePos.x < rect.top.right.x
+    &&
+    mousePos.y > rect.top.left.y
+    &&
+    mousePos.y < rect.bottom.right.y
+)
+
+if (mouseDown && inObject) canMoveObject = true
+
+if (!mouseDown) canMoveObject = false
+
+
+
+
+ +
+ + + + \ No newline at end of file