From 8f43cf3a018787d2d3026c99a36b767f95e95992 Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Fri, 14 Feb 2025 14:14:40 +0100 Subject: [PATCH] skycraft: can walk around after landing, sort of. --- skycraft/chunk.ts | 2 +- skycraft/draw.ts | 48 +++++------ skycraft/index.ts | 205 ++++++++++++++++++++++++++++++++++++++++------ skycraft/orbit.ts | 3 +- 4 files changed, 201 insertions(+), 57 deletions(-) diff --git a/skycraft/chunk.ts b/skycraft/chunk.ts index 3b85008..8107357 100644 --- a/skycraft/chunk.ts +++ b/skycraft/chunk.ts @@ -329,7 +329,7 @@ export function getBodyGeometry(seed: number) { }; } -function blockLookup(chunkMap, x, y, z) { +export function blockLookup(chunkMap, x, y, z) { const chunki = Math.floor((x + CHUNKSIZE / 2) / CHUNKSIZE); const chunkj = Math.floor((y + CHUNKSIZE / 2) / CHUNKSIZE); const chunkk = Math.floor((z + CHUNKSIZE / 2) / CHUNKSIZE); diff --git a/skycraft/draw.ts b/skycraft/draw.ts index 383963a..24e531b 100644 --- a/skycraft/draw.ts +++ b/skycraft/draw.ts @@ -122,8 +122,7 @@ export async function initWorldGl(gl: WebGLRenderingContext) { const drawObject = (objectParams) => { const { - position, - orientation, + tf, glBuffer, numVertices, lightDirection, @@ -131,8 +130,7 @@ export async function initWorldGl(gl: WebGLRenderingContext) { specularColor, } = objectParams; - gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.product( - se3.translation(...position), orientation))); + gl.uniformMatrix4fv(modelLoc, false, new Float32Array(tf)); gl.uniform3fv(lightDirectionLoc, lightDirection); gl.uniform3fv(glowColorLoc, glowColor); gl.uniform3fv(specularColorLoc, specularColor); @@ -219,14 +217,12 @@ export function getOrbitDrawContext(gl: WebGLRenderingContext) { const drawObject = (objectParams) => { const { - position, - orientation, + tf, value, glBuffer, } = objectParams; - gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.product( - se3.translation(...position), orientation))); + gl.uniformMatrix4fv(modelLoc, false, new Float32Array(tf)); gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); @@ -244,6 +240,16 @@ export function getOrbitDrawContext(gl: WebGLRenderingContext) { }; } +function getShipTf(context) { + if (context.landed) { + const body = context.landedBody; + const bodyTf = se3.product(se3.translation(...body.position), body.orientation); + return se3.product(bodyTf, context.shipTf); + } + + return context.player.tf; +} + function getObjects(context, body, parentPosition = undefined) { const objects = []; const {gl, glContext, player} = context; @@ -255,8 +261,7 @@ function getObjects(context, body, parentPosition = undefined) { objects.push({ geometry: body.glBuffer, - orientation, - position, + tf: se3.product(se3.translation(...position), orientation), glContext, glowColor, specularColor, @@ -265,23 +270,10 @@ function getObjects(context, body, parentPosition = undefined) { const orbitObject = makeOrbitObject(gl, context.orbitGlContext, body.orbit, parentPosition); //objects.push(orbitObject); } else { - const tfs = [player.tf]; - if (context.landed) { - const body = context.landedBody; - const bodyTf = se3.product(se3.translation(...body.position), body.orientation); - tfs.splice(0, 0, bodyTf); - } - const tf = tfs.reduce(se3.product); - const shipOrientation = [ - se3.rotationOnly(tf), - //se3.rotationOnly(context.camera.tf), - se3.rotxyz(-Math.PI / 2, Math.PI / 2, Math.PI / 2), - ].reduce(se3.product); - const shipPos = se3.apply(tf, [0, 0, 0, 1]); + const shipTf = se3.product(getShipTf(context), se3.rotxyz(-Math.PI / 2, Math.PI / 2, Math.PI / 2)); objects.push({ geometry: makeBufferFromFaces(gl, context.spaceship), - orientation: shipOrientation, - position: shipPos, + tf: shipTf, glContext, specularColor: [0.8, 0.8, 0.8], }); @@ -334,7 +326,7 @@ export function draw(context) { const viewMatrix = se3.inverse(playerView.reduce(se3.product)); let lastGlContext; - for (const {position, orientation, geometry, glContext, glowColor, specularColor} of objects) { + for (const {tf, geometry, glContext, glowColor, specularColor} of objects) { if (glContext !== lastGlContext) { glContext.setupScene({ projectionMatrix: projMatrix, @@ -344,11 +336,11 @@ export function draw(context) { } lastGlContext = glContext; + const position = se3.apply(tf, [0, 0, 0, 1]); const lightDirection = sunDirection(position); glContext.drawObject({ - position, - orientation, + tf, glBuffer: geometry.glBuffer, numVertices: geometry.numVertices, lightDirection, diff --git a/skycraft/index.ts b/skycraft/index.ts index b9ade5a..6d61b75 100644 --- a/skycraft/index.ts +++ b/skycraft/index.ts @@ -4,7 +4,7 @@ import * as linalg from './linalg'; import { loadObjModel } from './obj'; import * as se3 from 'wmc-common/se3'; import { computeOrbit, findSoi, getCartesianState, updateBodyPhysics, isInSoi } from './orbit'; -import { getBodyGeometry, castRay, BlockType } from './chunk'; +import { getBodyGeometry, castRay, blockLookup, BlockType } from './chunk'; import { draw, getOrbitDrawContext, initWorldGl } from './draw'; import * as quat from './quat'; @@ -159,9 +159,14 @@ function initUiListeners(canvas: HTMLCanvasElement, context) { case 'KeyL': context.landing = !context.landing; return false; - case 'Space': + case 'KeyP': context.pause = !context.pause; return false; + case 'Space': + if (!context.flying) { + jump(context); + } + return false; } } else { context.keys.delete(e.code); @@ -204,29 +209,80 @@ function initUiListeners(canvas: HTMLCanvasElement, context) { }); } +function jump(context) { + if (context.jumpAmount > 0) { + const amount = context.jumpForce; + context.jumpAmount -= 1; + + const gravity = landedGravity(context); + const up = linalg.normalize(linalg.scale(gravity, -1)); + context.player.velocity = linalg.scale(up, amount); + } +} + +function moveShip(context, forward, right) { + if (context.keys.has('ShiftLeft')) { + forward *= 10; + right *= 10; + } + const tf = se3.product( + se3.orientationOnly(context.player.tf), + context.camera.tf, + ); + const dir = [right, 0, -forward, 10]; + if (context.flying) { + context.player.tf = [ + context.player.tf, + se3.translation(...dir), + ].reduce(se3.product); + } else { + const vel = context.player.velocity; + const dv = linalg.scale(se3.apply(tf, dir), 1/dir[3]); + context.player.velocity = linalg.add(vel, dv); + delete context.orbit; + } +} + +function moveOnGround(context, forward, right) { + if (context.keys.has('ShiftLeft')) { + forward *= 2; + right *= 2; + } + const tf = se3.product( + se3.orientationOnly(context.player.tf), + context.camera.tf, + ); + const dir = [right, 0, -forward, 0]; + if (context.flying) { + context.player.tf = [ + context.player.tf, + se3.translation(...dir), + ].reduce(se3.product); + return; + } + + const dv = linalg.scale(se3.apply(tf, dir), 10); + const maxSpeed = 8; + const airMovement = 0.08; + + if (context.isOnGround) { + context.player.velocity = dv; + return; + } + + // steering in the air + context.player.velocity = linalg.add(context.player.velocity, linalg.scale(dv, airMovement)); + + const curVel = linalg.norm(context.player.velocity); + + if (curVel > maxSpeed) { + context.player.velocity = linalg.scale(context.player.velocity, maxSpeed / curVel); + } +} + + function handleInput(context) { - const move = (forward: number, right: number) => { - if (context.keys.has('ShiftLeft')) { - forward *= 10; - right *= 10; - } - const tf = se3.product( - se3.orientationOnly(context.player.tf), - context.camera.tf, - ); - const dir = [right, 0, -forward, 10]; - if (context.flying) { - context.player.tf = [ - context.player.tf, - se3.translation(...dir), - ].reduce(se3.product); - } else { - const vel = context.player.velocity; - const dv = linalg.scale(se3.apply(tf, dir), 1/dir[3]); - context.player.velocity = linalg.add(vel, dv); - delete context.orbit; - } - }; + const move = (f, r) => (context.landed ? moveOnGround : moveShip)(context, f, r); const roll = (amount: number) => { if (context.keys.has('ShiftLeft')) { @@ -312,6 +368,9 @@ function finishLanding(context, body) { se3.inverse(se3.translation(...body.position)), p.tf, ].reduce(se3.product); + + context.shipTf = p.tf; + p.tf = se3.product(p.tf, se3.translation(1, 1, 0)); p.velocity = [0, 0, 0]; p.position = se3.apply(p.tf, [0, 0, 0, 1]); } @@ -407,6 +466,100 @@ function effectiveGravity(position: number[], rootBody: Body) : number[] { return acceleration; } +function landedGravity(context) { + // just clamp "down" to one of our six directions + const pos = context.player.position; + const altitude2 = pos[0]**2 + pos[1]**2 + pos[2]**2; + const g = context.landedBody.mass / altitude2; + if (pos[0] > 0 && Math.abs(pos[1]) < pos[0] && Math.abs(pos[2]) < pos[0]) { + return [-g, 0, 0]; + } + if (pos[0] < 0 && Math.abs(pos[1]) < -pos[0] && Math.abs(pos[2]) < -pos[0]) { + return [+g, 0, 0]; + } + if (pos[1] > 0 && Math.abs(pos[0]) < pos[1] && Math.abs(pos[2]) < pos[1]) { + return [0, -g, 0]; + } + if (pos[1] < 0 && Math.abs(pos[0]) < -pos[1] && Math.abs(pos[2]) < -pos[1]) { + return [0, +g, 0]; + } + if (pos[2] > 0 && Math.abs(pos[1]) < pos[2] && Math.abs(pos[1]) < pos[2]) { + return [0, 0, -g]; + } + if (pos[2] < 0 && Math.abs(pos[1]) < -pos[2] && Math.abs(pos[1]) < -pos[2]) { + return [0, 0, +g]; + } + // blarg +} + +function checkCollision(curPos, newPos, chunkMap, orientation) { + const upDir = se3.apply(orientation, [0, 1, 0, 0]); + // I guess Gaspard is about 1.7 m tall? + // he also has a 60x60 cm axis-aligned square section '^_^ + // box is centered around the camera + const gaspardBB = [ + [-0.3, 0.2, -0.3], + [-0.3, 0.2, 0.3], + [0.3, 0.2, -0.3], + [0.3, 0.2, 0.3], + + [-0.3, -1.5, -0.3], + [-0.3, -1.5, 0.3], + [0.3, -1.5, -0.3], + [0.3, -1.5, 0.3], + ].map(v => se3.apply(orientation, v.concat([0]))); + + let dp = linalg.diff(newPos, curPos); + let isOnGround = false; + + for (let i = 0; i < 3; i++) { + const newGaspard = v => v.map((x, j) => i === j ? x + newPos[j] : x + curPos[j]); + for (const point of gaspardBB.map(newGaspard)) { + const block = blockLookup(chunkMap, ...point); + if (block.type !== BlockType.AIR) { + const dir = [...Array(3).keys()].map(j => j === i ? 1 : 0); + if (Math.abs(linalg.dot(dir, upDir)) > 0.5) { + isOnGround = true; + } + dp[i] = 0; + } + } + } + for (let i = 0; i < 3; i++) { + const newGaspard = v => linalg.add(linalg.add(curPos, v), dp); + for (const point of gaspardBB.map(newGaspard)) { + const block = blockLookup(chunkMap, ...point); + if (block.type !== BlockType.AIR) { + dp[i] = 0; + } + } + } + + return { + newPos: linalg.add(curPos, dp), + isOnGround, + }; +} + +function updateLandedPhysics(context, dt) { + const gravity = landedGravity(context); + context.player.velocity = linalg.add(context.player.velocity, linalg.scale(gravity, dt)); + + const oldPos = context.player.position; + const taregtPos = linalg.add(context.player.position, linalg.scale(context.player.velocity, dt)); + + // from wmc + const {isOnGround, newPos} = checkCollision(oldPos, taregtPos, context.landedBody.geometry.chunkMap, se3.orientationOnly(context.player.tf)); + context.player.position = newPos; + context.player.velocity = newPos.map((p, i) => (p - oldPos[i]) / dt); + if (isOnGround) { + context.jumpAmount = 2; + context.player.velocity = context.player.velocity.map(v => v * 0.7); + } + context.isOnGround = isOnGround; + context.player.tf = se3.setPosition(context.player.tf, newPos); +} + function updatePhysics(time: number, context) { const {player} = context; const dt = time - (context.lastTime || 0); @@ -421,7 +574,7 @@ function updatePhysics(time: number, context) { const kShipRotateSpeed = 0.1; if (context.landed) { - return; // TODO: implement + return updateLandedPhysics(context, dt); } if (context.landing) { @@ -505,7 +658,7 @@ async function main() { // TODO // [ ] loading bar // [x] spaceship - // [ ] landing + // [x] landing // [ ] huge planets // [x] lighting // [x] better lighting diff --git a/skycraft/orbit.ts b/skycraft/orbit.ts index 03f7b80..fdf3973 100644 --- a/skycraft/orbit.ts +++ b/skycraft/orbit.ts @@ -277,8 +277,7 @@ export function makeOrbitObject(gl: WebGLRenderingContext, glContext: any, orbit return { geometry, - orientation, - position, + tf: se3.product(se3.translation(...position), orientation), glContext, }; }