From 371d0368fed695b3ac1500dd29e253e53eb9e32d Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Sat, 11 Apr 2026 15:16:41 +1000 Subject: [PATCH 01/18] Update main.js --- main.js | 50 +++++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/main.js b/main.js index 8f7f527..0a8ca6e 100644 --- a/main.js +++ b/main.js @@ -184,15 +184,6 @@ function getViewMatrix(camera) { ].flat(); return camToWorld; } -// function translate4(a, x, y, z) { -// return [ -// ...a.slice(0, 12), -// a[0] * x + a[4] * y + a[8] * z + a[12], -// a[1] * x + a[5] * y + a[9] * z + a[13], -// a[2] * x + a[6] * y + a[10] * z + a[14], -// a[3] * x + a[7] * y + a[11] * z + a[15], -// ]; -// } function multiply4(a, b) { return [ @@ -1186,32 +1177,25 @@ async function main() { activeKeys.includes("ShiftLeft") || activeKeys.includes("ShiftRight"); - if (activeKeys.includes("ArrowUp")) { - if (shiftKey) { - inv = translate4(inv, 0, -0.03, 0); - } else { - inv = translate4(inv, 0, 0, 0.1); - } - } - if (activeKeys.includes("ArrowDown")) { - if (shiftKey) { - inv = translate4(inv, 0, 0.03, 0); - } else { - inv = translate4(inv, 0, 0, -0.1); - } - } - if (activeKeys.includes("ArrowLeft")) - inv = translate4(inv, -0.03, 0, 0); - // - if (activeKeys.includes("ArrowRight")) - inv = translate4(inv, 0.03, 0, 0); - // inv = rotate4(inv, 0.01, 0, 1, 0); - if (activeKeys.includes("KeyA")) inv = rotate4(inv, -0.01, 0, 1, 0); - if (activeKeys.includes("KeyD")) inv = rotate4(inv, 0.01, 0, 1, 0); + // WASD - First person movement + if (activeKeys.includes("KeyW")) inv = translate4(inv, 0, 0, 0.1); // Forward + if (activeKeys.includes("KeyS")) inv = translate4(inv, 0, 0, -0.1); // Backward + if (activeKeys.includes("KeyA")) inv = translate4(inv, -0.03, 0, 0); // Strafe left + if (activeKeys.includes("KeyD")) inv = translate4(inv, 0.03, 0, 0); // Strafe right + + // Space/Shift - Up/Down movement + if (activeKeys.includes("Space")) inv = translate4(inv, 0, -0.03, 0); // Up + if (shiftKey) inv = translate4(inv, 0, 0.03, 0); // Down + + // Arrow keys - Camera rotation (look around) + if (activeKeys.includes("ArrowLeft")) inv = rotate4(inv, -0.01, 0, 1, 0); // Look left + if (activeKeys.includes("ArrowRight")) inv = rotate4(inv, 0.01, 0, 1, 0); // Look right + if (activeKeys.includes("ArrowUp")) inv = rotate4(inv, 0.01, 1, 0, 0); // Look up + if (activeKeys.includes("ArrowDown")) inv = rotate4(inv, -0.01, 1, 0, 0); // Look down + + // Q/E - Roll camera (optional) if (activeKeys.includes("KeyQ")) inv = rotate4(inv, 0.01, 0, 0, 1); if (activeKeys.includes("KeyE")) inv = rotate4(inv, -0.01, 0, 0, 1); - if (activeKeys.includes("KeyW")) inv = rotate4(inv, 0.005, 1, 0, 0); - if (activeKeys.includes("KeyS")) inv = rotate4(inv, -0.005, 1, 0, 0); const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; let isJumping = activeKeys.includes("Space"); From f5f76f78299376e2b6d4fc92f52c4388ee29251a Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 15 Apr 2026 11:29:26 +1000 Subject: [PATCH 02/18] Update main.js --- main.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main.js b/main.js index 0a8ca6e..3c2a276 100644 --- a/main.js +++ b/main.js @@ -1466,3 +1466,12 @@ main().catch((err) => { document.getElementById("spinner").style.display = "none"; document.getElementById("message").innerText = err.toString(); }); + +window.addEventListener('mousedown', () => { + window.focus(); +}); + +// This helps when the mouse enters the iframe +window.addEventListener('mouseenter', () => { + window.focus(); +}); From 6a45d8ae8dd2d674b638f77bbfd8b63522c048b8 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 16 Apr 2026 11:39:18 +1000 Subject: [PATCH 03/18] Update index.html --- index.html | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index c25c895..f7e641e 100644 --- a/index.html +++ b/index.html @@ -198,23 +198,25 @@

WebGL 3D Gaussian Splat Viewer

By Kevin Kwok. Code on GithubGithub.

- Use mouse or arrow keys to navigate. + Use arrow keys (recommneded) or mouse to navigate. -
movement (arrow keys) -- left/right arrow keys to strafe side to side -- up/down arrow keys to move forward/back -- space to jump +
movement (WASD) +- A and D to strafe side to side +- W and S arrow keys to move forward/back +- space to jump / move up + +camera angle (arrow keys and QE) +- left and right arrow to turn camera left/right +- top and bottom arrow to tilt camera up/down -camera angle (wasd) -- a/d to turn camera left/right -- w/s to tilt camera up/down -- q/e to roll camera counterclockwise/clockwise -- i/k and j/l to orbit trackpad - scroll up/down/left/right to orbit @@ -226,6 +228,10 @@

WebGL 3D Gaussian Splat Viewer

- click and drag to orbit - right click (or ctrl/cmd key) and drag up/down to move +advanced: +- q/e to roll camera counterclockwise/clockwise +- i/k and j/l to orbit + touch (mobile) - one finger to orbit - two finger pinch to move forward/back From 744ff513fec294cad7fd731c5ba99f634c8e484d Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 16 Apr 2026 11:41:51 +1000 Subject: [PATCH 04/18] Update index.html --- index.html | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index f7e641e..350f9d1 100644 --- a/index.html +++ b/index.html @@ -191,20 +191,16 @@ -
-

WebGL 3D Gaussian Splat Viewer

-

- - By Kevin Kwok. - Code on - GithubGithub. - +

WebGL 3D Gaussian Splat Viewer

+

+ + By Kevin Kwok. + Code on Github. +
+ Forked by Jeremy Jiao. Code on + Github. +

-
Use arrow keys (recommneded) or mouse to navigate. From 003c7f6d2a183389ad6c64cdde7ca92dff5192dc Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 7 May 2026 10:10:00 +1000 Subject: [PATCH 05/18] Update main.js --- main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.js b/main.js index 3c2a276..077a877 100644 --- a/main.js +++ b/main.js @@ -1193,9 +1193,9 @@ async function main() { if (activeKeys.includes("ArrowUp")) inv = rotate4(inv, 0.01, 1, 0, 0); // Look up if (activeKeys.includes("ArrowDown")) inv = rotate4(inv, -0.01, 1, 0, 0); // Look down - // Q/E - Roll camera (optional) - if (activeKeys.includes("KeyQ")) inv = rotate4(inv, 0.01, 0, 0, 1); - if (activeKeys.includes("KeyE")) inv = rotate4(inv, -0.01, 0, 0, 1); + // Q/E - Roll camera (optional) modified from original inverted roll. + if (activeKeys.includes("KeyE")) inv = rotate4(inv, 0.01, 0, 0, 1); + if (activeKeys.includes("KeyQ")) inv = rotate4(inv, -0.01, 0, 0, 1); const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; let isJumping = activeKeys.includes("Space"); From 28258abf4e3e121e99b883c56dfa50a9f42dee9f Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 15:46:53 +1000 Subject: [PATCH 06/18] Update main.js --- main.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.js b/main.js index 077a877..060fe01 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,13 @@ + + let cameras = [ { id: 0, From d8caee4f071731b57b82342e6d85d023302d0554 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 15:49:49 +1000 Subject: [PATCH 07/18] Update main.js --- main.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/main.js b/main.js index 060fe01..8098c4c 100644 --- a/main.js +++ b/main.js @@ -1,12 +1,4 @@ - + let cameras = [ { From ea01adb646e3d1c5c6ef3246b565c0302e4693ba Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 15:53:56 +1000 Subject: [PATCH 08/18] Update main.js --- main.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 8098c4c..7de77d8 100644 --- a/main.js +++ b/main.js @@ -752,7 +752,28 @@ async function main() { const rowLength = 3 * 4 + 3 * 4 + 4 + 4; const reader = req.body.getReader(); - let splatData = new Uint8Array(req.headers.get("content-length")); + + // Fallback if content-length is missing or altered by Gzip compression + let contentLength = parseInt(req.headers.get("content-length")); + let splatData = new Uint8Array(contentLength || 0); + + // Stream the data chunks smoothly into a dynamic array builder + let bytesRead = 0; + let chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + bytesRead += value.length; + } + + // Combine all chunks into the final array accurately matching unpacked size + splatData = new Uint8Array(bytesRead); + let offset = 0; + for (let chunk of chunks) { + splatData.set(chunk, offset); + offset += chunk.length; + } const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; From f1b600be277e900cfa2cbe99d23b22b6a41cded5 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 16:04:49 +1000 Subject: [PATCH 09/18] Update main.js --- main.js | 55 +++++++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/main.js b/main.js index 7de77d8..c06513d 100644 --- a/main.js +++ b/main.js @@ -743,37 +743,36 @@ async function main() { "https://huggingface.co/cakewalk/splat-data/resolve/main/", ); const req = await fetch(url, { - mode: "cors", // no-cors, *cors, same-origin - credentials: "omit", // include, *same-origin, omit - }); - console.log(req); - if (req.status != 200) - throw new Error(req.status + " Unable to load " + req.url); + mode: "cors", // no-cors, *cors, same-origin + credentials: "omit", // include, *same-origin, omit + }); + console.log(req); + if (req.status != 200) + throw new Error(req.status + " Unable to load " + req.url); - const rowLength = 3 * 4 + 3 * 4 + 4 + 4; - const reader = req.body.getReader(); - - // Fallback if content-length is missing or altered by Gzip compression - let contentLength = parseInt(req.headers.get("content-length")); - let splatData = new Uint8Array(contentLength || 0); + const rowLength = 3 * 4 + 3 * 4 + 4 + 4; + const reader = req.body.getReader(); + + // HIDE ERROR: Allocate a massive flat placeholder array size (e.g. 150MB) + // This gives the compressed gzip stream chunks plenty of room to expand + // so it never overflows or throws an out-of-bounds error banner. + let splatData = new Uint8Array(150 * 1024 * 1024); + + let bytesRead = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Put the incoming data chunk directly into our oversized array + splatData.set(value, bytesRead); + bytesRead += value.length; + } + + // Shrink the array down to exactly how many bytes were actually read + // so WebGL doesn't try to render empty space + splatData = splatData.subarray(0, bytesRead); - // Stream the data chunks smoothly into a dynamic array builder - let bytesRead = 0; - let chunks = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - bytesRead += value.length; - } - // Combine all chunks into the final array accurately matching unpacked size - splatData = new Uint8Array(bytesRead); - let offset = 0; - for (let chunk of chunks) { - splatData.set(chunk, offset); - offset += chunk.length; - } const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; From 927a681af60c113eec96aff3d03d3e84f0b57fbf Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 16:07:04 +1000 Subject: [PATCH 10/18] Update main.js --- main.js | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/main.js b/main.js index c06513d..4e83612 100644 --- a/main.js +++ b/main.js @@ -729,6 +729,7 @@ let defaultViewMatrix = [ 0.03, 6.55, 1, ]; let viewMatrix = defaultViewMatrix; + async function main() { let carousel = true; const params = new URLSearchParams(location.search); @@ -736,44 +737,45 @@ async function main() { viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); carousel = false; } catch (err) {} + const url = new URL( // "nike.splat", // location.href, params.get("url") || "train.splat", "https://huggingface.co/cakewalk/splat-data/resolve/main/", ); + const req = await fetch(url, { - mode: "cors", // no-cors, *cors, same-origin - credentials: "omit", // include, *same-origin, omit - }); - console.log(req); - if (req.status != 200) - throw new Error(req.status + " Unable to load " + req.url); + mode: "cors", // no-cors, *cors, same-origin + credentials: "omit", // include, *same-origin, omit + }); + console.log(req); + if (req.status != 200) + throw new Error(req.status + " Unable to load " + req.url); - const rowLength = 3 * 4 + 3 * 4 + 4 + 4; - const reader = req.body.getReader(); + const rowLength = 3 * 4 + 3 * 4 + 4 + 4; + const reader = req.body.getReader(); + + // HIDE ERROR: Allocate a massive flat placeholder array size (e.g. 150MB) + // This gives the compressed gzip stream chunks plenty of room to expand + // so it never overflows or throws an out-of-bounds error banner. + let splatData = new Uint8Array(150 * 1024 * 1024); + + // FIX: Removed 'let' from bytesRead here to fix the "already been declared" SyntaxError + bytesRead = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; - // HIDE ERROR: Allocate a massive flat placeholder array size (e.g. 150MB) - // This gives the compressed gzip stream chunks plenty of room to expand - // so it never overflows or throws an out-of-bounds error banner. - let splatData = new Uint8Array(150 * 1024 * 1024); - - let bytesRead = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - // Put the incoming data chunk directly into our oversized array - splatData.set(value, bytesRead); - bytesRead += value.length; - } + // Put the incoming data chunk directly into our oversized array + splatData.set(value, bytesRead); + bytesRead += value.length; + } // Shrink the array down to exactly how many bytes were actually read // so WebGL doesn't try to render empty space splatData = splatData.subarray(0, bytesRead); - - const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; console.log(splatData.length / rowLength, downsample); From 1f863ac61a9889abd33b66e89356c3b94cda46be Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 16:09:22 +1000 Subject: [PATCH 11/18] Update main.js --- main.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/main.js b/main.js index 4e83612..077a877 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,3 @@ - - let cameras = [ { id: 0, @@ -729,7 +727,6 @@ let defaultViewMatrix = [ 0.03, 6.55, 1, ]; let viewMatrix = defaultViewMatrix; - async function main() { let carousel = true; const params = new URLSearchParams(location.search); @@ -737,14 +734,12 @@ async function main() { viewMatrix = JSON.parse(decodeURIComponent(location.hash.slice(1))); carousel = false; } catch (err) {} - const url = new URL( // "nike.splat", // location.href, params.get("url") || "train.splat", "https://huggingface.co/cakewalk/splat-data/resolve/main/", ); - const req = await fetch(url, { mode: "cors", // no-cors, *cors, same-origin credentials: "omit", // include, *same-origin, omit @@ -755,26 +750,7 @@ async function main() { const rowLength = 3 * 4 + 3 * 4 + 4 + 4; const reader = req.body.getReader(); - - // HIDE ERROR: Allocate a massive flat placeholder array size (e.g. 150MB) - // This gives the compressed gzip stream chunks plenty of room to expand - // so it never overflows or throws an out-of-bounds error banner. - let splatData = new Uint8Array(150 * 1024 * 1024); - - // FIX: Removed 'let' from bytesRead here to fix the "already been declared" SyntaxError - bytesRead = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - // Put the incoming data chunk directly into our oversized array - splatData.set(value, bytesRead); - bytesRead += value.length; - } - - // Shrink the array down to exactly how many bytes were actually read - // so WebGL doesn't try to render empty space - splatData = splatData.subarray(0, bytesRead); + let splatData = new Uint8Array(req.headers.get("content-length")); const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; From 21c329de14cb8a87e793254c8948852075c59e5d Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 16:14:35 +1000 Subject: [PATCH 12/18] Update main.js --- main.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 077a877..7de77d8 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,5 @@ + + let cameras = [ { id: 0, @@ -750,7 +752,28 @@ async function main() { const rowLength = 3 * 4 + 3 * 4 + 4 + 4; const reader = req.body.getReader(); - let splatData = new Uint8Array(req.headers.get("content-length")); + + // Fallback if content-length is missing or altered by Gzip compression + let contentLength = parseInt(req.headers.get("content-length")); + let splatData = new Uint8Array(contentLength || 0); + + // Stream the data chunks smoothly into a dynamic array builder + let bytesRead = 0; + let chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + bytesRead += value.length; + } + + // Combine all chunks into the final array accurately matching unpacked size + splatData = new Uint8Array(bytesRead); + let offset = 0; + for (let chunk of chunks) { + splatData.set(chunk, offset); + offset += chunk.length; + } const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; From 3bb73449a88c4e15bed6d2889e1c71698187b8f8 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Wed, 10 Jun 2026 16:25:25 +1000 Subject: [PATCH 13/18] Update main.js --- main.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/main.js b/main.js index 7de77d8..077a877 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,3 @@ - - let cameras = [ { id: 0, @@ -752,28 +750,7 @@ async function main() { const rowLength = 3 * 4 + 3 * 4 + 4 + 4; const reader = req.body.getReader(); - - // Fallback if content-length is missing or altered by Gzip compression - let contentLength = parseInt(req.headers.get("content-length")); - let splatData = new Uint8Array(contentLength || 0); - - // Stream the data chunks smoothly into a dynamic array builder - let bytesRead = 0; - let chunks = []; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - bytesRead += value.length; - } - - // Combine all chunks into the final array accurately matching unpacked size - splatData = new Uint8Array(bytesRead); - let offset = 0; - for (let chunk of chunks) { - splatData.set(chunk, offset); - offset += chunk.length; - } + let splatData = new Uint8Array(req.headers.get("content-length")); const downsample = splatData.length / rowLength > 500000 ? 1 : 1 / devicePixelRatio; From 5fc713121f9f7553e68d7ff6a95b1b6c3ffc573f Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 18 Jun 2026 21:20:11 +1000 Subject: [PATCH 14/18] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9b391f..62b7cc0 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ https://github.com/antimatter15/splat/assets/30054/878d5d34-e0a7-4336-85df-111ff -## controls +## controls +This was the main feature of this fork, just game-like controls. movement (arrow keys) - left/right arrow keys to strafe side to side From 4bb32b605c3b43ed1d21e4afb4119e8668cb9219 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 9 Jul 2026 21:30:58 +1000 Subject: [PATCH 15/18] Implement roll lock for camera in touch controls Added roll lock functionality to prevent camera rolling during touch interactions. --- main.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index 077a877..1213e68 100644 --- a/main.js +++ b/main.js @@ -1083,20 +1083,31 @@ async function main() { let inv = invert4(viewMatrix); let dx = (4 * (e.touches[0].clientX - startX)) / innerWidth; let dy = (4 * (e.touches[0].clientY - startY)) / innerHeight; - let d = 4; inv = translate4(inv, 0, 0, d); - // inv = translate4(inv, -x, -y, -z); - // inv = translate4(inv, x, y, z); + // inv = translate4(inv, -x, -y, -z); + // inv = translate4(inv, x, y, z); inv = rotate4(inv, dx, 0, 1, 0); inv = rotate4(inv, -dy, 1, 0, 0); inv = translate4(inv, 0, 0, -d); viewMatrix = invert4(inv); + // === ADD THIS ROLL LOCK (prevents camera rolling) === + inv = invert4(viewMatrix); + inv[2] = 0; // forward X + inv[6] = 0; // forward Y + inv[10] = 1; // forward Z + inv[1] = 0; // up X + inv[5] = 1; // up Y + inv[9] = 0; // up Z + viewMatrix = invert4(inv); + // =================================================== + startX = e.touches[0].clientX; startY = e.touches[0].clientY; - } else if (e.touches.length === 2) { + } + else if (e.touches.length === 2) { // alert('beep') const dtheta = Math.atan2(startY - altY, startX - altX) - From 3c9242adabc375d09022ba249addae086aa43e37 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 9 Jul 2026 21:33:49 +1000 Subject: [PATCH 16/18] Implement camera roll lock to maintain level view Added logic to keep the camera level by recalculating the right and up vectors based on the forward direction. --- main.js | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/main.js b/main.js index 1213e68..eaaa733 100644 --- a/main.js +++ b/main.js @@ -1093,15 +1093,38 @@ async function main() { viewMatrix = invert4(inv); - // === ADD THIS ROLL LOCK (prevents camera rolling) === - inv = invert4(viewMatrix); - inv[2] = 0; // forward X - inv[6] = 0; // forward Y - inv[10] = 1; // forward Z - inv[1] = 0; // up X - inv[5] = 1; // up Y - inv[9] = 0; // up Z + + // Force the camera to stay level + let forward = [inv[2], inv[6], inv[10]]; + let up = [0, 1, 0]; // world up + + // Rebuild right vector + let right = [ + forward[1] * up[2] - forward[2] * up[1], + forward[2] * up[0] - forward[0] * up[2], + forward[0] * up[1] - forward[1] * up[0] + ]; + + // Normalize + let len = Math.hypot(right[0], right[1], right[2]); + right[0] /= len; + right[1] /= len; + right[2] /= len; + + // Rebuild up vector properly + up = [ + right[2] * forward[1] - right[1] * forward[2], + right[0] * forward[2] - right[2] * forward[0], + right[1] * forward[0] - right[0] * forward[1] + ]; + + // Write back to matrix + inv[0] = right[0]; inv[4] = right[1]; inv[8] = right[2]; + inv[1] = up[0]; inv[5] = up[1]; inv[9] = up[2]; + inv[2] = forward[0]; inv[6] = forward[1]; inv[10] = forward[2]; + viewMatrix = invert4(inv); + // ============================= // =================================================== startX = e.touches[0].clientX; From b85a501fa7be0e17d4c356504674915dd23cb829 Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 9 Jul 2026 21:38:29 +1000 Subject: [PATCH 17/18] Update main.js --- main.js | 56 +++++++++++++++++++++++--------------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/main.js b/main.js index eaaa733..7b34114 100644 --- a/main.js +++ b/main.js @@ -1080,56 +1080,46 @@ async function main() { (e) => { e.preventDefault(); if (e.touches.length === 1 && down) { - let inv = invert4(viewMatrix); let dx = (4 * (e.touches[0].clientX - startX)) / innerWidth; let dy = (4 * (e.touches[0].clientY - startY)) / innerHeight; + + let inv = invert4(viewMatrix); + + // Simpler orbit with better roll control let d = 4; inv = translate4(inv, 0, 0, d); - // inv = translate4(inv, -x, -y, -z); - // inv = translate4(inv, x, y, z); - inv = rotate4(inv, dx, 0, 1, 0); - inv = rotate4(inv, -dy, 1, 0, 0); + + inv = rotate4(inv, dx, 0, 1, 0); // Yaw + inv = rotate4(inv, -dy, 1, 0, 0); // Pitch + inv = translate4(inv, 0, 0, -d); viewMatrix = invert4(inv); + // Strong roll correction + inv = invert4(viewMatrix); + const forward = [inv[2], inv[6], inv[10]]; + const right = [ + forward[1], + -forward[0], + 0 + ].map((v, i, a) => v / Math.hypot(a[0], a[1])); - // Force the camera to stay level - let forward = [inv[2], inv[6], inv[10]]; - let up = [0, 1, 0]; // world up - - // Rebuild right vector - let right = [ - forward[1] * up[2] - forward[2] * up[1], - forward[2] * up[0] - forward[0] * up[2], - forward[0] * up[1] - forward[1] * up[0] + const up = [ + right[1] * forward[2] - right[2] * forward[1], + right[2] * forward[0] - right[0] * forward[2], + right[0] * forward[1] - right[1] * forward[0] ]; - - // Normalize - let len = Math.hypot(right[0], right[1], right[2]); - right[0] /= len; - right[1] /= len; - right[2] /= len; - - // Rebuild up vector properly - up = [ - right[2] * forward[1] - right[1] * forward[2], - right[0] * forward[2] - right[2] * forward[0], - right[1] * forward[0] - right[0] * forward[1] - ]; - - // Write back to matrix + inv[0] = right[0]; inv[4] = right[1]; inv[8] = right[2]; inv[1] = up[0]; inv[5] = up[1]; inv[9] = up[2]; inv[2] = forward[0]; inv[6] = forward[1]; inv[10] = forward[2]; - + viewMatrix = invert4(inv); - // ============================= - // =================================================== startX = e.touches[0].clientX; startY = e.touches[0].clientY; - } + } else if (e.touches.length === 2) { // alert('beep') const dtheta = From de8c69ea81af1d42434ac35012b1bc93d8f1924d Mon Sep 17 00:00:00 2001 From: Jeremy Jiao <144148@caulfieldgs.vic.edu.au> Date: Thu, 9 Jul 2026 21:57:05 +1000 Subject: [PATCH 18/18] Updated WASD controls --- main.js | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/main.js b/main.js index 7b34114..077a877 100644 --- a/main.js +++ b/main.js @@ -1080,47 +1080,23 @@ async function main() { (e) => { e.preventDefault(); if (e.touches.length === 1 && down) { + let inv = invert4(viewMatrix); let dx = (4 * (e.touches[0].clientX - startX)) / innerWidth; let dy = (4 * (e.touches[0].clientY - startY)) / innerHeight; - let inv = invert4(viewMatrix); - - // Simpler orbit with better roll control let d = 4; inv = translate4(inv, 0, 0, d); - - inv = rotate4(inv, dx, 0, 1, 0); // Yaw - inv = rotate4(inv, -dy, 1, 0, 0); // Pitch - + // inv = translate4(inv, -x, -y, -z); + // inv = translate4(inv, x, y, z); + inv = rotate4(inv, dx, 0, 1, 0); + inv = rotate4(inv, -dy, 1, 0, 0); inv = translate4(inv, 0, 0, -d); viewMatrix = invert4(inv); - // Strong roll correction - inv = invert4(viewMatrix); - const forward = [inv[2], inv[6], inv[10]]; - const right = [ - forward[1], - -forward[0], - 0 - ].map((v, i, a) => v / Math.hypot(a[0], a[1])); - - const up = [ - right[1] * forward[2] - right[2] * forward[1], - right[2] * forward[0] - right[0] * forward[2], - right[0] * forward[1] - right[1] * forward[0] - ]; - - inv[0] = right[0]; inv[4] = right[1]; inv[8] = right[2]; - inv[1] = up[0]; inv[5] = up[1]; inv[9] = up[2]; - inv[2] = forward[0]; inv[6] = forward[1]; inv[10] = forward[2]; - - viewMatrix = invert4(inv); - startX = e.touches[0].clientX; startY = e.touches[0].clientY; - } - else if (e.touches.length === 2) { + } else if (e.touches.length === 2) { // alert('beep') const dtheta = Math.atan2(startY - altY, startX - altX) -