From: A-Larsen Date: Thu, 15 Oct 2026 01:48:44 +0000 (-0400) Subject: first commit X-Git-Url: https://nyquistdev.net/git/?a=commitdiff_plain;h=e48e655f0585a60e356f67bd356cd16a8d310500;p=basic-3d-example.git first commit --- diff --git a/README.adoc b/README.adoc index ac1ab78..ae0655a 100644 --- a/README.adoc +++ b/README.adoc @@ -2,11 +2,11 @@ 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. +A good explaination 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 +NOTE: This started off as code from Tsoding, and then I used chatgpt to change + specific things. This is a learning tool! not something I wrote off the + top of my head == Graphics Pipeline @@ -19,9 +19,6 @@ vertex -> 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]]: @@ -37,7 +34,6 @@ y' &= y/z ++++ == 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): @@ -127,8 +123,6 @@ 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]. @@ -347,68 +341,8 @@ 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] diff --git a/example.html b/example.html index 28ab5d0..3a1f17c 100644 --- a/example.html +++ b/example.html @@ -58,14 +58,14 @@ body {
-       
+      
     
- + - - - - + + diff --git a/index.js b/index.js index 9336828..5f97d1d 100644 --- a/index.js +++ b/index.js @@ -1,229 +1,376 @@ -/* ===================== 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]; -} +// Meshes ------------------------------------------------- +const CUBE_MESH = [ + // front face + {x: -1, y: 1, z: -1}, + {x: 1, y: 1, z: -1}, + {x: 1, y: -1, z: -1}, + + {x: 1, y: -1, z: -1}, + {x: -1, y: -1, z: -1}, + {x: -1, y: 1, z: -1}, + + // backface + {x: -1, y: 1, z: 1}, + {x: 1, y: 1, z: 1}, + {x: 1, y: -1, z: 1}, + + {x: 1, y: -1, z: 1}, + {x: -1, y: -1, z: 1}, + {x: -1, y: 1, z: 1}, + + + // left face + {x: -1, y: 1, z: -1}, + {x: -1, y: 1, z: 1}, + {x: -1, y: -1, z: 1}, + + {x: -1, y: -1, z: 1}, + {x: -1, y: -1, z: -1}, + {x: -1, y: 1, z: -1}, + + // right face + {x: 1, y: 1, z: -1}, + {x: 1, y: 1, z: 1}, + {x: 1, y: -1, z: 1}, + + {x: 1, y: -1, z: 1}, + {x: 1, y: -1, z: -1}, + {x: 1, y: 1, z: -1}, + + // top face + {x: -1, y: 1, z: -1}, + {x: 1, y: 1, z: -1}, + {x: -1, y: 1, z: 1}, + + // bottom face + {x: -1, y: -1, z: -1}, + {x: 1, y: -1, z: -1}, + {x: -1, y: -1, z: 1}, + +] + +const PYRAMID_MESH = [ + // front face + {x: 0, y: 1, z: 0}, + {x: -1, y: -1, z: 1}, + {x: 1, y: -1, z: 1}, + + // right face + {x: 0, y: 1, z: 0}, + {x: 1, y: -1, z: 1}, + {x: 1, y: -1, z: -1}, + + // back face + {x: 0, y: 1, z: 0}, + {x: 1, y: -1, z: -1}, + {x: -1, y: -1, z: -1}, + + // left face + {x: 0, y: 1, z: 0}, + {x: -1, y: -1, z: -1}, + {x: -1, y: -1, z: 1}, + + // bottom (two triangles) + {x: -1, y: -1, z: 1}, + {x: 1, y: -1, z: 1}, + {x: 1, y: -1, z: -1}, + + {x: 1, y: -1, z: -1}, + {x: -1, y: -1, z: -1}, + {x: -1, y: -1, z: 1}, +] + +const DIAMOND_MESH = [ + // top + {x: 0,y: 1,z: 0},{x:-1,y: 0,z: 1},{x: 1,y: 0,z: 1}, + {x: 0,y: 1,z: 0},{x: 1,y: 0,z: 1},{x: 1,y: 0,z:-1}, + {x: 0,y: 1,z: 0},{x: 1,y: 0,z:-1},{x:-1,y: 0,z:-1}, + {x: 0,y: 1,z: 0},{x:-1,y: 0,z:-1},{x:-1,y: 0,z: 1}, + + // bottom + {x: 0,y:-1,z: 0},{x: 1,y: 0,z: 1},{x:-1,y: 0,z: 1}, + {x: 0,y:-1,z: 0},{x: 1,y: 0,z:-1},{x: 1,y: 0,z: 1}, + {x: 0,y:-1,z: 0},{x:-1,y: 0,z:-1},{x: 1,y: 0,z:-1}, + {x: 0,y:-1,z: 0},{x:-1,y: 0,z: 1},{x:-1,y: 0,z:-1}, +] + +const TETRAHEDRON_MESH = [ + // face 1 + {x: 1, y: 1, z: 1}, + {x: -1, y: -1, z: 1}, + {x: -1, y: 1, z: -1}, + + // face 2 + {x: 1, y: 1, z: 1}, + {x: -1, y: -1, z: 1}, + {x: 1, y: -1, z: -1}, + + // face 3 + {x: 1, y: 1, z: 1}, + {x: -1, y: 1, z: -1}, + {x: 1, y: -1, z: -1}, + + // face 4 (base) + {x: -1, y: -1, z: 1}, + {x: -1, y: 1, z: -1}, + {x: 1, y: -1, z: -1}, +] + +const OCTAHEDRON_MESH = [ + {x: 0,y: 1,z: 0},{x:-1,y: 0,z: 0},{x: 0,y: 0,z: 1}, + {x: 0,y: 1,z: 0},{x: 0,y: 0,z: 1},{x: 1,y: 0,z: 0}, + {x: 0,y: 1,z: 0},{x: 1,y: 0,z: 0},{x: 0,y: 0,z:-1}, + {x: 0,y: 1,z: 0},{x: 0,y: 0,z:-1},{x:-1,y: 0,z: 0}, + + {x: 0,y:-1,z: 0},{x: 0,y: 0,z: 1},{x:-1,y: 0,z: 0}, + {x: 0,y:-1,z: 0},{x: 1,y: 0,z: 0},{x: 0,y: 0,z: 1}, + {x: 0,y:-1,z: 0},{x: 0,y: 0,z:-1},{x: 1,y: 0,z: 0}, + {x: 0,y:-1,z: 0},{x:-1,y: 0,z: 0},{x: 0,y: 0,z:-1}, +] + +// Global constant variables ------------------------------ +const CTX = game.getContext("2d") +const VERTSIZE = 10 +const FOREGROUND_COLOR = "#b3ff66" +const BOUNDRIES_COLOR = "#FFFFFF" +const TEXT_COLOR = "#FFFFFF" +const AXIS_MODES = ["none", "yz", "xz", "xy"] +const KEYS = {}; +const NEAR = 0.1 +const FPS = 60 +const DT = 1/FPS + +// Other global variables ---------------------------------- +let meshes = [CUBE_MESH,PYRAMID_MESH, DIAMOND_MESH, TETRAHEDRON_MESH, OCTAHEDRON_MESH] +let mesh = meshes[0] +let inObject = false +let canMoveObject = false +let angle = Math.PI +let position = {x: 0, y: 0, z: 8} +let rotation = "none" +let mouseDown = false +let mousePos = {x: 0, y: 0} +let showCollisionBox = true +let moveObject = false +let objectScale = 0.25 +let triangle = [] +let ri = 0 +let mi = 0 +let ti = -1 + +// Canvas changes ------------------------------------------ +game.width = 800 +game.height = 800 + +// Controls ------------------------------------------------ +window.addEventListener("keydown", e => { + if (KEYS[e.key.toLowerCase()]) return + KEYS[e.key.toLowerCase()] = true + + if (KEYS["r"]) { + changeRotation() + } + if (KEYS["d"]) { + changeMesh() + } +}); + +window.addEventListener("keyup", e => KEYS[e.key.toLowerCase()] = false); + +game.addEventListener("mousedown", e => { + if (e.button === 0) { // left mouse button + mouseDown = true + } +}) + +game.addEventListener("mouseup", e => { + if (e.button === 0) { + mouseDown = false + } +}) + +game.addEventListener("mousemove", e => { + let m = getMousePos(e) + if (canMoveObject) { + position.x = (m.x/game.width * 2 - 1) * position.z + position.y = ((1 - m.y/game.height) * 2 - 1) * position.z + } + mousePos.x = m.x; + mousePos.y = m.y; +}) + +game.addEventListener("wheel", e => { + e.preventDefault() // prevent page from scrolling -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 - ]; + if (e.deltaY < 0) { + position.z += 0.4 + } else if (e.deltaY > 0) { + position.z -= 0.4 + } +}) + +// Control Functions --------------------------------------- +function getMousePos(e) { + const rect = game.getBoundingClientRect() + + return { + x: (e.clientX - rect.left) * (game.width / rect.width), + y: (e.clientY - rect.top) * (game.height / rect.height), + } } -function quatConjugate(q) { - return [q[0], -q[1], -q[2], -q[3]]; +// Screen drawing functions -------------------------------- +function clear() { + CTX.fillStyle = "black" + CTX.fillRect(0, 0, game.width, game.height) + + CTX.fillStyle = TEXT_COLOR; + CTX.font = "14px monospace"; + CTX.fillText("press r to change rotation", 10, 18); + CTX.fillText("press d to change mesh", 10, 36); + CTX.fillText("click and drag to move object" , 10, 54); + CTX.fillText("scroll to change objects z position" , 10, 72); } -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] }; +function point({x, y}, color) { + CTX.fillStyle = color + CTX.fillRect(x - VERTSIZE / 2,y - VERTSIZE/2, VERTSIZE, VERTSIZE) } -/* ===================== CAMERA VECTORS ===================== */ -function cameraForward() { - return quatRotateVector(camera.rotation, {x:0,y:0,z:1}); +function line(a, b, color) { + CTX.strokeStyle = color + CTX.lineWidth = 2 + CTX.beginPath() + CTX.moveTo(a.x , a.y ) + CTX.lineTo(b.x , b.y) + CTX.stroke() } -function cameraRight() { - return quatRotateVector(camera.rotation, {x:1,y:0,z:0}); + +// Mesh functions ------------------------------------------ +function changeMesh() { + mesh = meshes[(mi += 1)%meshes.length] } -function cameraUp() { - return quatRotateVector(camera.rotation, {x:0,y:1,z:0}); + +// Rotation functions -------------------------------------- +function rotate({x, y, z}, axis, angle) { + // rotating a vector + const c = Math.cos(angle) + const s = Math.sin(angle) + switch (axis) { + case "none": return {x: x, y: y, z: z} + case "yz": return {x, y:y*c-z*s, z: y*s+z*c} // x-axis + case "xz": return {x: x*c-z*s, y, z: x*s+z*c} // y-axis + case "xy": return {x: x*c-y*s, y: x*s+y*c, z} // z-axis + } } -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); +function changeRotation() { + rotation = AXIS_MODES[(ri += 1)%AXIS_MODES.length] } -/* ===================== PROJECTION ===================== */ -function project(p) { - return { x: p.x / p.z, y: p.y / p.z }; +// Graphics pipeline functions ----------------------------- +function project({ x, y, z }) { + const eps = 0.001 + return { x: x / Math.max(z, eps), y: y / Math.max(z, eps), z } } -function screen(p) { + +function screen({x, y}) { + // -1 .. 1 -> 0 .. w/h return { - x: (p.x + 1) * 0.5 * game.width, - y: (1 - (p.y + 1) * 0.5) * game.height - }; + x: ((x + 1) * 0.5 * game.width) , + y: ((1 - (y + 1) * 0.5) * game.width), + } } -/* ===================== 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 makeTriangles (vert, color) { + ti = (ti + 1) % 3 + triangle.push(vert) -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(); + if (ti != 2) return + for (let ti = 0; ti < 3; ++ti) { + let a = triangle[ti] + let b = triangle[(ti+1)%3] + // if (a.z <= NEAR || b.z <= NEAR) continue + line(a, b, color) + } + triangle = [] } -/* ===================== 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); +function translate(p1, p2) { + return { + x: p1.x + p2.x, + y: p1.y + p2.y, + z: p1.z + p2.z } +} - // --- 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; +// Bounds checking functions ------------------------------- +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} +} - clear(); +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}}, - for (const e of edges) { - for (let i=0;i rect.top.left.x + && + mousePos.x < rect.top.right.x + && + mousePos.y > rect.top.left.y + && + mousePos.y < rect.bottom.right.y + ) -/* ===================== INTERACTION ===================== */ -game.onclick = () => { - game.requestPointerLock(); - axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length; - activeAxes = AXIS_MODES[axisModeIndex]; -}; + if (mouseDown && inObject) canMoveObject = true -window.addEventListener("mousemove", e => { - if (document.pointerLockElement !== game) return; + if (!mouseDown) canMoveObject = false - 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); + setTimeout(frame, 1000/FPS) +} - camera.rotation = quatMultiply(yaw, camera.rotation); - camera.rotation = quatMultiply(camera.rotation, pitch); -}); +frame()