diff --git a/skycraft/chunk.ts b/skycraft/chunk.ts new file mode 100644 index 0000000..8397c29 --- /dev/null +++ b/skycraft/chunk.ts @@ -0,0 +1,313 @@ +import { makeFace } from '../geometry'; +import * as linalg from './linalg'; +import {memoize} from '../memoize'; + +type direction = ('-x' | '+x' | '-y' | '+y' | '-z' | '+z'); + +const BlockType = { + UNDEFINED: 0, + AIR: 1, + DIRT: 2, + GRASS: 3, + STONE: 4, + WATER: 5, + TREE: 6, + LEAVES: 7, + SUN: 8, +}; + +const CHUNKSIZE = 32; + + +/** seed: some kind of number uniquely defining the body + * x, y, z: space coordinates in the body's frame + * + * returns: a chunk + */ + function makeDirtBlock(seed: number) { + // XXX: for now, return a premade chunk: a 24x24x24 cube of dirt + // surrounded by 4 blocks of air all around + const cs = CHUNKSIZE; + + if (seed !== 1337) { + return {}; + } +// if (Math.abs + + const blocks = new Array(cs * cs * cs); + blocks.fill(BlockType.AIR); + + const dirt = new Array(24).fill(BlockType.DIRT); + + for (let i = 0; i < 24; i++) { + for (let j = 4; j < 28; j++) { + const offset = cs * cs * (i + 4) + cs * j; + blocks.splice(offset + 4, 24, ...dirt); + } + } + + const half = cs / 2; + + return { + position: [-half, -half, -half], + blocks, + underground: false, + seed, + }; +} + +function makeSunChunk(seed: number, i: number, j: number, k: number) { + const cs = CHUNKSIZE; + const radius = 42; + if (Math.abs(cs * i) > radius + || Math.abs(cs * j) > radius + || Math.abs(cs * k) > radius) { + return undefined; + } + const half = cs / 2; + const blocks = new Array(cs**3); + blocks.fill(BlockType.SUN); + + let underground = true; + + for (let x = 0; x < cs; x++) { + for (let y = 0; y < cs; y++) { + for (let z = 0; z < cs; z++) { + const pos = [ + x + i * cs - half, + y + j * cs - half, + z + k * cs - half, + ]; + const idx = ( + z * cs * cs + + y * cs + + x + ); + if (pos[0]**2 + pos[1]**2 + pos[2]**2 > radius**2) { + blocks[idx] = BlockType.AIR; + underground = false; + } + } + } + } + + return { + position: [i * cs - half, j * cs - half, k * cs - half], + layout: [i, j, k], + blocks, + seed, + underground, + }; +} + +function _getChunk(seed: number, chunkX: number, chunkY: number, chunkZ: number) { + if (seed === 0) { + return makeSunChunk(seed, chunkX, chunkY, chunkZ); + } + if (chunkX === 0 && chunkY === 0 && chunkZ === 0) { + return makeDirtBlock(seed); // x, y, z unused right now + } + return undefined; +} +const getChunk = memoize(_getChunk); + +function faceTexture(type: number, dir: direction) { + switch (type) { + case BlockType.GRASS: + switch (dir) { + case '+y': return [0, 15]; + case '-y': return [2, 15]; + default: return [1, 15]; + } + case BlockType.DIRT: return [2, 15]; + case BlockType.STONE: return [3, 15]; + case BlockType.WATER: return [4, 15]; + case BlockType.TREE: + switch (dir) { + case '+y': + case '-y': + return [5, 15]; + default: return [6, 15]; + } + case BlockType.LEAVES: return [7, 15]; + case BlockType.SUN: return [0, 4]; + default: return [0, 0]; + } +} + +function* makeChunkFaces(chunk) { + const cs = CHUNKSIZE; + + function faceCenter(pos: linalg.vec3, dir: direction) { + switch (dir) { + case '-x': return [pos[0] - 0.5, pos[1], pos[2]]; + case '+x': return [pos[0] + 0.5, pos[1], pos[2]]; + case '-y': return [pos[0], pos[1] - 0.5, pos[2]]; + case '+y': return [pos[0], pos[1] + 0.5, pos[2]]; + case '-z': return [pos[0], pos[1], pos[2] - 0.5]; + case '+z': return [pos[0], pos[1], pos[2] + 0.5]; + } + } + + function neighborChunk(dir: direction) { + const [chunkX, chunkY, chunkZ] = chunk.layout; + if (chunk.neighbors === undefined) { + chunk.neighbors = {}; + } + if (!(dir in chunk.neighbors)) { + switch (dir) { + case '-x': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX - 1, chunkY, chunkZ); + break; + case '+x': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX + 1, chunkY, chunkZ); + break; + case '-y': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY - 1, chunkZ); + break; + case '+y': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY + 1, chunkZ); + break; + case '-z': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ - 1); + break; + case '+z': + chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ + 1); + break; + } + } + return chunk.neighbors[dir]; + } + + const neighborIndices = { + '-x': (x, y, z) => z * cs * cs + y * cs + (cs - 1), + '+x': (x, y, z) => z * cs * cs + y * cs + 0, + '-y': (x, y, z) => z * cs * cs + (cs - 1) * cs + x, + '+y': (x, y, z) => z * cs * cs + 0 * cs + x, + '-z': (x, y, z) => (cs - 1) * cs * cs + y * cs + x, + '+z': (x, y, z) => 0 * cs * cs + y * cs + x, + }; + + function neighborBlock(dir: direction, x, y, z) { + const neighbor = neighborChunk(dir); + let block; + if (neighbor === undefined) { + block = BlockType.AIR; + } else { + block = neighbor.blocks[neighborIndices[dir](x, y, z)]; + } + return { dir, block }; + } + + function* neighbors(x, y, z) { + const idx = ( + z * cs * cs + + y * cs + + x + ); + if (x > 0) { + yield { + block: chunk.blocks[idx - 1], + dir: '-x', + }; + } else { + yield neighborBlock('-x', x, y, z); + } + if (x < cs - 1) { + yield { + block: chunk.blocks[idx + 1], + dir: '+x', + }; + } else { + yield neighborBlock('+x', x, y, z); + } + if (y > 0) { + yield { + block: chunk.blocks[idx - cs], + dir: '-y', + }; + } else { + yield neighborBlock('-y', x, y, z); + } + if (y < cs - 1) { + yield { + block: chunk.blocks[idx + cs], + dir: '+y', + }; + } else { + yield neighborBlock('+y', x, y, z); + } + if (z > 0) { + yield { + block: chunk.blocks[idx - cs * cs], + dir: '-z', + }; + } else { + yield neighborBlock('-z', x, y, z); + } + if (z < cs - 1) { + yield { + block: chunk.blocks[idx + cs * cs], + dir: '+z', + }; + } else { + yield neighborBlock('+z', x, y, z); + } + } + + for (let x = 0; x < cs; x++) { + for (let y = 0; y < cs; y++) { + for (let z = 0; z < cs; z++) { + const idx = ( + z * cs * cs + + y * cs + + x + ); + const chpos = chunk.position; + const bkpos = [ + chpos[0] + x, + chpos[1] + y, + chpos[2] + z, + ]; + const bt = chunk.blocks[idx]; + if (bt === BlockType.AIR) { + continue; + } + for (const { block, dir } of neighbors(x, y, z)) { + if (block !== BlockType.AIR) { + continue; + } + yield makeFace(dir, faceTexture(bt, dir), faceCenter(bkpos, dir)); + } + } + } + } +} + +function getBodyChunks(seed: number) { + const chunks = []; + const toCheck = [[0, 0, 0]]; + while (toCheck.length > 0) { + const [chunkX, chunkY, chunkZ] = toCheck.pop(); + const thisChunk = getChunk(seed, chunkX, chunkY, chunkZ); + if (thisChunk === undefined || chunks.includes(thisChunk)) { + continue; + } + chunks.push(thisChunk); + toCheck.push([chunkX - 1, chunkY, chunkZ]); + toCheck.push([chunkX + 1, chunkY, chunkZ]); + toCheck.push([chunkX, chunkY - 1, chunkZ]); + toCheck.push([chunkX, chunkY + 1, chunkZ]); + toCheck.push([chunkX, chunkY, chunkZ - 1]); + toCheck.push([chunkX, chunkY, chunkZ + 1]); + } + return chunks; +} + +export function getBodyGeometry(seed: number) { + const faces = getBodyChunks(seed) + .filter(chunk => !chunk.underground) + .map(chunk => [...makeChunkFaces(chunk)]); + + return faces.reduce((a, b) => a.concat(b)); +} \ No newline at end of file diff --git a/skycraft/index.js b/skycraft/index.ts similarity index 53% rename from skycraft/index.js rename to skycraft/index.ts index bb1ebcc..0953a7b 100644 --- a/skycraft/index.js +++ b/skycraft/index.ts @@ -1,12 +1,10 @@ -//import { initUiListeners, setupParamPanel, tick } from './game'; -//import { initWorldGl, makeWorld } from './world'; -import * as se3 from '../se3'; -import {loadTexture, makeProgram} from '../gl'; -import {makeFace, makeBufferFromFaces} from '../geometry'; -import { loadStlModel } from './stl'; +import { makeFace, makeBufferFromFaces } from '../geometry'; +import { loadTexture, makeProgram } from '../gl'; import * as linalg from './linalg'; -import { memoize } from '../memoize'; import { loadObjModel } from './obj'; +import * as se3 from '../se3'; +import { computeOrbit, findSoi, getCartesianState, makeOrbitObject, updateBodyPhysics } from './orbit'; +import { getBodyGeometry } from './chunk'; const VSHADER = ` attribute vec3 aPosition; @@ -80,7 +78,7 @@ void main() { const kEpoch = 0; -async function initWorldGl(gl) { +async function initWorldGl(gl: WebGLRenderingContext) { const program = makeProgram(gl, VSHADER, FSHADER); const texture = await loadTexture(gl, 'texture.png'); @@ -243,298 +241,6 @@ function getOrbitDrawContext(gl) { }; } -const BlockType = { - UNDEFINED: 0, - AIR: 1, - DIRT: 2, - GRASS: 3, - STONE: 4, - WATER: 5, - TREE: 6, - LEAVES: 7, - SUN: 8, -}; - -const CHUNKSIZE = 32; - -/** seed: some kind of number uniquely defining the body - * x, y, z: space coordinates in the body's frame - * - * returns: a chunk - */ -function makeDirtBlock(seed, x, y, z) { - // XXX: for now, return a premade chunk: a 24x24x24 cube of dirt - // surrounded by 4 blocks of air all around - const cs = CHUNKSIZE; - - if (seed !== 1337) { - return {}; - } -// if (Math.abs - - const blocks = new Array(cs * cs * cs); - blocks.fill(BlockType.AIR); - - const dirt = new Array(24).fill(BlockType.DIRT); - - for (let i = 0; i < 24; i++) { - for (let j = 4; j < 28; j++) { - const offset = cs * cs * (i + 4) + cs * j; - blocks.splice(offset + 4, 24, ...dirt); - } - } - - const half = cs / 2; - - return { - position: [-half, -half, -half], - blocks, - underground: false, - seed, - }; -} - -function makeSunChunk(seed, i, j, k) { - const cs = CHUNKSIZE; - const radius = 42; - if (Math.abs(cs * i) > radius - || Math.abs(cs * j) > radius - || Math.abs(cs * k) > radius) { - return undefined; - } - const half = cs / 2; - const blocks = new Array(cs**3); - blocks.fill(BlockType.SUN); - - let underground = true; - - for (let x = 0; x < cs; x++) { - for (let y = 0; y < cs; y++) { - for (let z = 0; z < cs; z++) { - const pos = [ - x + i * cs - half, - y + j * cs - half, - z + k * cs - half, - ]; - const idx = ( - z * cs * cs + - y * cs + - x - ); - if (pos[0]**2 + pos[1]**2 + pos[2]**2 > radius**2) { - blocks[idx] = BlockType.AIR; - underground = false; - } - } - } - } - - return { - position: [i * cs - half, j * cs - half, k * cs - half], - layout: [i, j, k], - blocks, - seed, - underground, - }; -} - -function _getChunk(seed, chunkX, chunkY, chunkZ) { - if (seed === 0) { - return makeSunChunk(seed, chunkX, chunkY, chunkZ); - } - if (chunkX === 0 && chunkY === 0 && chunkZ === 0) { - return makeDirtBlock(seed); // x, y, z unused right now - } - return undefined; -} -const getChunk = memoize(_getChunk); - -function getBodyChunks(seed) { - const chunks = []; - const toCheck = [[0, 0, 0]]; - while (toCheck.length > 0) { - const [chunkX, chunkY, chunkZ] = toCheck.pop(); - const thisChunk = getChunk(seed, chunkX, chunkY, chunkZ); - if (thisChunk === undefined || chunks.includes(thisChunk)) { - continue; - } - chunks.push(thisChunk); - toCheck.push([chunkX - 1, chunkY, chunkZ]); - toCheck.push([chunkX + 1, chunkY, chunkZ]); - toCheck.push([chunkX, chunkY - 1, chunkZ]); - toCheck.push([chunkX, chunkY + 1, chunkZ]); - toCheck.push([chunkX, chunkY, chunkZ - 1]); - toCheck.push([chunkX, chunkY, chunkZ + 1]); - } - return chunks; -} - -function faceTexture(type, dir) { - switch (type) { - case BlockType.GRASS: - switch (dir) { - case '+y': return [0, 15]; - case '-y': return [2, 15]; - default: return [1, 15]; - } - case BlockType.DIRT: return [2, 15]; - case BlockType.STONE: return [3, 15]; - case BlockType.WATER: return [4, 15]; - case BlockType.TREE: - switch (dir) { - case '+y': - case '-y': - return [5, 15]; - default: return [6, 15]; - } - case BlockType.LEAVES: return [7, 15]; - case BlockType.SUN: return [0, 4]; - default: return [0, 0]; - } -} - -function* makeChunkFaces(chunk) { - const cs = CHUNKSIZE; - - function faceCenter(pos, dir) { - switch (dir) { - case '-x': return [pos[0] - 0.5, pos[1], pos[2]]; - case '+x': return [pos[0] + 0.5, pos[1], pos[2]]; - case '-y': return [pos[0], pos[1] - 0.5, pos[2]]; - case '+y': return [pos[0], pos[1] + 0.5, pos[2]]; - case '-z': return [pos[0], pos[1], pos[2] - 0.5]; - case '+z': return [pos[0], pos[1], pos[2] + 0.5]; - } - } - - function neighborChunk(dir) { - const [chunkX, chunkY, chunkZ] = chunk.layout; - if (chunk.neighbors === undefined) { - chunk.neighbors = {}; - } - if (!(dir in chunk.neighbors)) { - if (dir === '-x') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX - 1, chunkY, chunkZ); - } else if (dir === '+x') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX + 1, chunkY, chunkZ); - } else if (dir === '-y') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY - 1, chunkZ); - } else if (dir === '+y') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY + 1, chunkZ); - } else if (dir === '-z') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ - 1); - } else if (dir === '+z') { - chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ + 1); - } - } - return chunk.neighbors[dir]; - } - - const neighborIndices = { - '-x': (x, y, z) => z * cs * cs + y * cs + (cs - 1), - '+x': (x, y, z) => z * cs * cs + y * cs + 0, - '-y': (x, y, z) => z * cs * cs + (cs - 1) * cs + x, - '+y': (x, y, z) => z * cs * cs + 0 * cs + x, - '-z': (x, y, z) => (cs - 1) * cs * cs + y * cs + x, - '+z': (x, y, z) => 0 * cs * cs + y * cs + x, - }; - - function neighborBlock(dir, x, y, z) { - const neighbor = neighborChunk(dir); - let block; - if (neighbor === undefined) { - block = BlockType.AIR; - } else { - block = neighbor.blocks[neighborIndices[dir](x, y, z)]; - } - return { dir, block }; - } - - function* neighbors(x, y, z) { - const idx = ( - z * cs * cs + - y * cs + - x - ); - if (x > 0) { - yield { - block: chunk.blocks[idx - 1], - dir: '-x', - }; - } else { - yield neighborBlock('-x', x, y, z); - } - if (x < cs - 1) { - yield { - block: chunk.blocks[idx + 1], - dir: '+x', - }; - } else { - yield neighborBlock('+x', x, y, z); - } - if (y > 0) { - yield { - block: chunk.blocks[idx - cs], - dir: '-y', - }; - } else { - yield neighborBlock('-y', x, y, z); - } - if (y < cs - 1) { - yield { - block: chunk.blocks[idx + cs], - dir: '+y', - }; - } else { - yield neighborBlock('+y', x, y, z); - } - if (z > 0) { - yield { - block: chunk.blocks[idx - cs * cs], - dir: '-z', - }; - } else { - yield neighborBlock('-z', x, y, z); - } - if (z < cs - 1) { - yield { - block: chunk.blocks[idx + cs * cs], - dir: '+z', - }; - } else { - yield neighborBlock('+z', x, y, z); - } - } - - for (let x = 0; x < cs; x++) { - for (let y = 0; y < cs; y++) { - for (let z = 0; z < cs; z++) { - const idx = ( - z * cs * cs + - y * cs + - x - ); - const chpos = chunk.position; - const bkpos = [ - chpos[0] + x, - chpos[1] + y, - chpos[2] + z, - ]; - const bt = chunk.blocks[idx]; - if (bt === BlockType.AIR) { - continue; - } - for (const {block, dir} of neighbors(x, y, z)) { - if (block !== BlockType.AIR) { - continue; - } - yield makeFace(dir, faceTexture(bt), faceCenter(bkpos, dir)); - } - } - } - } -} - function closeToPlanet(context) { const body = findSoi(context); const relativePos = linalg.diff(context.player.position, body.position); @@ -542,15 +248,7 @@ function closeToPlanet(context) { return linalg.norm(relativePos) < 20; } -function getBodyGeometry(seed) { - const faces = getBodyChunks(seed) - .filter(chunk => !chunk.underground) - .map(chunk => [...makeChunkFaces(chunk)]); - - return faces.reduce((a, b) => a.concat(b)); -} - -function getSolarSystem(seed) { +function getSolarSystem(seed: number) { /// XXX: only returns 1 body for now return { @@ -663,7 +361,7 @@ function getSolarSystem(seed) { }; } -function initUiListeners(canvas, context) { +function initUiListeners(canvas: HTMLCanvasElement, context) { const canvasClickHandler = () => { canvas.requestPointerLock(); canvas.onclick = null; @@ -745,7 +443,7 @@ function initUiListeners(canvas, context) { } function handleInput(context) { - const move = (forward, right) => { + const move = (forward: number, right: number) => { if (context.keys.has('ShiftLeft')) { forward *= 10; right *= 10; @@ -792,138 +490,7 @@ function handleInput(context) { }); } -function updateBodyPhysics(time, body, parentBody) { - if (parentBody !== undefined) { - const mu = parentBody.mass; - const {position, velocity} = getCartesianState(body.orbit, mu, time); - body.position = [ - parentBody.position[0] + position[0], - parentBody.position[1] + position[1], - parentBody.position[2] + position[2], - ]; - body.velocity = [ - parentBody.velocity[0] + velocity[0], - parentBody.velocity[1] + velocity[1], - parentBody.velocity[2] + velocity[2], - ]; - } else { - body.position = [0, 0, 0]; - body.velocity = [0, 0, 0]; - } - body.orientation = getOrientation(body, time); - - if (body.children !== undefined) { - for (const child of body.children) { - updateBodyPhysics(time, child, body); - } - } -} - -function findSoi(context) { - const bodies = [context.universe]; - let body; - while (bodies.length > 0) { - body = bodies.shift(); - - if (body.children === undefined) { - return body; - } - - for (const child of body.children) { - const soi = child.orbit.semimajorAxis * Math.pow(child.mass / body.mass, 2/5); - const pos = context.player.position; - const bod = child.position; - const dr = [pos[0] - bod[0], pos[1] - bod[1], pos[2] - bod[2]]; - if (dr[0]**2 + dr[1]**2 + dr[2]**2 < soi**2) { - bodies.push(child); - } - } - } - - return body; -} - -function computeOrbit(player, body, time) { - const {cross, diff, norm, dot, scale} = linalg; - const rvec = diff(player.position, body.position); - const r = norm(rvec); - - if (norm(player.velocity) < 1e-6) { - // cheating - console.log('cheating'); - player.velocity = scale(cross([1, 1, 1], rvec), 0.01/(r**2)); - } - const vvec = diff(player.velocity, body.velocity); - const v = norm(vvec); - const Hvec = cross(rvec, vvec); - const H = norm(Hvec); - - const mu = body.mass; - const p = H**2 / mu; - const resinnu = Math.sqrt(p/mu) * dot(vvec, rvec) - const recosnu = p - r; - - const e = Math.sqrt(resinnu**2 + recosnu**2) / r; - - // should also work for hyperbolic orbits - const a = p/(1-e**2); - - const x = scale(rvec, 1/r); - const yy = cross(Hvec, rvec); - const y = scale(yy, 1/norm(yy)); - const z = scale(Hvec, 1/H); - - // Om i and w can be skipped when we just give tf... - let Om = Math.atan2(Hvec[0], -Hvec[1]); - if (Hvec[0] === 0 && Hvec[1] === 0) { - Om = 0; - } - - let i = Math.atan2(Math.sqrt(Hvec[0]**2 + Hvec[1]**2), Hvec[2]); - if (i * Math.sign((Hvec[0] || 1) * Math.sin(Om)) < 0) { - i *= -1; - } - - const nu = Math.atan2(resinnu, recosnu); - - let w = Math.atan2(rvec[2] / r / Math.sin(i), y[2] / Math.sin(i)) || 0 - nu; - - let t0; - let E; - - if (a < 0) { - E = 2 * Math.atanh(Math.sqrt((e-1)/(e+1)) * Math.tan(nu/2)); - const n = Math.sqrt(-mu / (a**3)); - t0 = time - (e*Math.sinh(E) - E) / n; - } else { - E = 2 * Math.atan(Math.sqrt((1-e)/(1+e)) * Math.tan(nu/2)); - const n = Math.sqrt(mu / (a**3)); - t0 = time - (1/n)*(E - e*Math.sin(E)); - } - - // column-major... see se3.js - const tf = se3.product([ - x[0], x[1], x[2], 0, - y[0], y[1], y[2], 0, - z[0], z[1], z[2], 0, - 0, 0, 0, 1, - ], se3.rotz(-nu)); - - const orbit = { - excentricity: e, - semimajorAxis: a, - inclination: i, - ascendingNodeLongitude: Om, - periapsisArgument: w, - t0, - tf, - lastE: E, - }; - - return orbit; -} - -function updatePhysics(time, context) { +function updatePhysics(time: number, context) { const {player} = context; const dt = time - (context.lastTime || 0); context.lastTime = time; @@ -938,7 +505,7 @@ function updatePhysics(time, context) { const dx = linalg.diff(newPos, player.position); player.tf = se3.product(se3.translation(...dx), player.tf); - const body = findSoi(context); + const body = findSoi(context.universe, context.player.position); context.orbit = computeOrbit(player, body, time); console.log(`orbiting ${body.name}, excentricity: ${context.orbit.excentricity}`); context.orbitBody = body; @@ -964,139 +531,7 @@ function updatePhysics(time, context) { function updateGeometry(context, timeout_ms = 10) { } -function normalizeAngle(theta) { - const twopi = 2 * Math.PI; - return theta - twopi * Math.floor((theta + Math.PI) / twopi); -} - -/** Let's be honest I should clean this up. - * - * This is the part that solves Kepler's equation using Newton's method. - * For circular-ish orbits, one or two iterations are usually enough. - * More excentric orbits can take more (6 or 7?). - * - * For near-parabolic orbits (and some others?) it often fails to converge... - */ -function getCartesianState(orbit, mu, time) { - const { - excentricity: e, - semimajorAxis: a, - inclination: i, - ascendingNodeLongitude: Om, - periapsisArgument: w, - t0, - } = orbit; - let n = Math.sqrt(mu/(a**3)); - if (a < 0) { - n = Math.sqrt(mu/-(a**3)); // mean motion - } - const M = n * (time - t0); // mean anomaly - - // Newton's method - var E2 = 0; - var E = orbit.lastE || M; - let iterations = 0; - // a clever guess? https://link.springer.com/article/10.1023/A:1008200607490 - // doesn't work at all. - - while (Math.abs(E - E2) > 1e-10) { - if (e < 0.001) { - break; - } - E = E2; - if (e < 1) { - E2 = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); - } else if (e > 1) { - E2 = E - (-E + e * Math.sinh(E) - M) / (e * Math.cosh(E) - 1); - } else { - E2 = E - (E + E*E*E/3 - M) / (1 + E*E); - } - - iterations++; - if (iterations > 100) { - console.log('numerical instability'); - return {}; - } - } - orbit.lastE = E; - let nu; - if (e > 1) { - nu = 2 * Math.atan(Math.sqrt((e+1) / (e-1)) * Math.tanh(E/2)); - } else { - nu = 2 * Math.atan(Math.sqrt((1+e) / (1-e)) * Math.tan(E/2)); - } - const p = a * (1 - e**2); - const r = p / (1 + e * Math.cos(nu));// * ((a < 0) ? -1 : 1); - const rd = e * Math.sqrt(mu / p) * Math.sin(nu); - - if (orbit.tf === undefined) { - // FIXME: this is actually borken. :/ - orbit.tf = [se3.rotz(Om), se3.rotx(i), se3.rotz(w)].reduce(se3.product); - } - - const tf = se3.product(orbit.tf, se3.rotz(nu)); - const pos = se3.apply(tf, [r, 0, 0, 1]); - const vel = se3.apply(tf, [rd, Math.sqrt(p * mu) / r, 0, 1]); - - return { - position: pos.slice(0, 3), - velocity: vel.slice(0, 3), - }; -} - -function getOrientation(body, time) { - return se3.rotxyz( - body.spin[0] * time, - body.spin[1] * time, - body.spin[2] * time, - ); -} - -function makeOrbitObject(context, orbit, parentPosition) { - const {gl} = context; - const position = parentPosition; - const glContext = context.orbitGlContext; - const orientation = orbit.tf; - - // FIXME: currently borken. - // const orientation = [ - // se3.rotz(orbit.ascendingNodeLongitude), - // se3.rotx(orbit.inclination), - // se3.rotz(orbit.periapsisArgument), - // ].reduce(se3.product); - - const buffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - - const a = orbit.semimajorAxis; - const b = a * Math.sqrt(1 - orbit.excentricity**2); - - const x = - orbit.semimajorAxis * orbit.excentricity; - - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ - x-a, -b, 0, -1, -1, - x-a, +b, 0, -1, +1, - x+a, -b, 0, +1, -1, - x+a, +b, 0, +1, +1, - ]), gl.STATIC_DRAW); - - const geometry = { - glBuffer: buffer, - numVertices: 4, - delete: () => gl.deleteBuffer(buffer), - }; - - return { - geometry, - orientation, - position, - glContext, - }; -} - -const kGravitationalConstant = 6.674e-11; - -function getObjects(context, body, parentPosition) { +function getObjects(context, body, parentPosition = undefined) { const objects = []; const {gl, glContext, player} = context; const {position, orientation, glowColor} = body; @@ -1113,7 +548,7 @@ function getObjects(context, body, parentPosition) { glowColor, }); if (parentPosition !== undefined) { - const orbitObject = makeOrbitObject(context, body.orbit, parentPosition); + const orbitObject = makeOrbitObject(gl, context.orbitGlContext, body.orbit, parentPosition); objects.push(orbitObject); } else { const shipOrientation = [ @@ -1139,18 +574,19 @@ function getObjects(context, body, parentPosition) { return objects; } -function sunDirection(context, position) { +function sunDirection(position: linalg.vec3) { return linalg.scale(position, 1/linalg.norm(position)); } function draw(context) { - const {gl, camera, player, universe} = context; + const {gl, camera, player, universe, orbit, orbitGlContext, orbitBody} = context; + const {skyColor, ambiantLight, projMatrix} = context; const objects = getObjects(context, universe); - if (context.orbit !== undefined && context.orbit.excentricity < 1) { - objects.push(makeOrbitObject(context, context.orbit, context.orbitBody.position)); + if (orbit !== undefined && orbit.excentricity < 1) { + objects.push(makeOrbitObject(gl, orbitGlContext, orbit, orbitBody.position)); } - gl.clearColor(...context.skyColor, 1.0); + gl.clearColor(...skyColor, 1.0); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); @@ -1163,9 +599,8 @@ function draw(context) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const viewMatrix = se3.inverse([ - context.player.tf, // player position & orientation - - context.camera.tf, // camera orientation relative to player + player.tf, // player position & orientation + camera.tf, // camera orientation relative to player se3.translation(0, 1, 4), // step back from the player ].reduce(se3.product)); let lastGlContext; @@ -1173,14 +608,14 @@ function draw(context) { for (const {position, orientation, geometry, glContext, glowColor} of objects) { if (glContext !== lastGlContext) { glContext.setupScene({ - projectionMatrix: context.projMatrix, + projectionMatrix: projMatrix, viewMatrix, - ambiantLightAmount: context.ambiantLight, + ambiantLightAmount: ambiantLight, }); } lastGlContext = glContext; - const lightDirection = sunDirection(context, position); + const lightDirection = sunDirection(position); glContext.drawObject({ position, @@ -1193,7 +628,7 @@ function draw(context) { } } -function tick(time, context) { +function tick(time: number, context) { handleInput(context); const simTime = time * 0.001 + context.timeOffset; updatePhysics(simTime, context); @@ -1213,7 +648,7 @@ function tick(time, context) { const dt = (time - context.lastFrameTime) * 0.001; context.lastFrameTime = time; - document.querySelector('#fps').textContent = `${1.0 / dt} fps`; + document.querySelector('#fps')!.textContent = `${1.0 / dt} fps`; requestAnimationFrame(time => tick(time, context)); } @@ -1229,27 +664,8 @@ function makeCube(texture) { ]; } -function makeObjects(gl) { - const texture = [0, 4]; - const faces = [ - makeFace('-x', texture, [-0.5, 0, 0]), - makeFace('+x', texture, [+0.5, 0, 0]), - makeFace('-y', texture, [0, -0.5, 0]), - makeFace('+y', texture, [0, +0.5, 0]), - makeFace('-z', texture, [0, 0, -0.5]), - makeFace('+z', texture, [0, 0, +0.5]), - ]; - return [ - { - geometry: makeBufferFromFaces(gl, faces), - orientation: [0, 0, 0], - position: [0, 0, 0], - }, - ]; -} - async function main() { - const canvas = document.querySelector('#game'); + const canvas = document.querySelector('#game')! as HTMLCanvasElement; // adjust canvas aspect ratio to that of the screen canvas.height = screen.height / screen.width * canvas.width; const gl = canvas.getContext('webgl'); @@ -1291,7 +707,6 @@ async function main() { isOnGround: false, gravity: -17, jumpForce: 6.5, -// objects: makeObjects(gl), universe: getSolarSystem(0), timeOffset: 0, }; @@ -1300,7 +715,6 @@ async function main() { context.orbitGlContext = getOrbitDrawContext(gl); initUiListeners(canvas, context); -// setupParamPanel(context); const starshipGeom = await modelPromise; console.log(`loaded ${starshipGeom.length} triangles`); diff --git a/skycraft/linalg.js b/skycraft/linalg.ts similarity index 59% rename from skycraft/linalg.js rename to skycraft/linalg.ts index 0dfae12..70bdfaf 100644 --- a/skycraft/linalg.js +++ b/skycraft/linalg.ts @@ -1,4 +1,6 @@ -export function cross(a, b) { +export type vec3 = [number, number, number]; + +export function cross(a: vec3, b: vec3) : vec3 { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], @@ -6,7 +8,7 @@ export function cross(a, b) { ]; } -export function diff(a, b) { +export function diff(a: vec3, b: vec3) : vec3 { return [ a[0] - b[0], a[1] - b[1], @@ -14,7 +16,7 @@ export function diff(a, b) { ]; } -export function add(a, b) { +export function add(a: vec3, b: vec3) : vec3 { return [ a[0] + b[0], a[1] + b[1], @@ -22,14 +24,14 @@ export function add(a, b) { ]; } -export function norm(a) { +export function norm(a: vec3) : number { return Math.sqrt(a[0] ** 2 + a[1] ** 2 + a[2] ** 2); } -export function dot(a, b) { +export function dot(a: vec3, b: vec3) : number { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } -export function scale(a, s) { +export function scale(a: vec3, s: number) : vec3 { return [ a[0] * s, a[1] * s, diff --git a/skycraft/obj.js b/skycraft/obj.ts similarity index 89% rename from skycraft/obj.js rename to skycraft/obj.ts index 54f14b5..73c58be 100644 --- a/skycraft/obj.js +++ b/skycraft/obj.ts @@ -1,4 +1,4 @@ -function parseObjLine(line, obj) { +function parseObjLine(line: string, obj: any) { line = line.trim(); if (line[0] === '#' || line.length < 1) { return; @@ -7,7 +7,7 @@ function parseObjLine(line, obj) { obj[elements[0]].push(elements.slice(1)); } -function getFaces(obj) { +function getFaces(obj: any) { return obj.f.map(f => { const face = { 'vertices': [], @@ -29,9 +29,11 @@ function getFaces(obj) { }); } -export async function loadObjModel(url) { +export async function loadObjModel(url: string) { const stlDataStream = (await fetch(url)).body; - const faces = []; + if (stlDataStream === null) { + return Promise.reject(new Error(`Could not fetch ${url}`)); + } const obj = new Proxy({}, { get: (target, name) =>{ if (!(name in target)) { diff --git a/skycraft/orbit.ts b/skycraft/orbit.ts new file mode 100644 index 0000000..80add15 --- /dev/null +++ b/skycraft/orbit.ts @@ -0,0 +1,283 @@ +import * as se3 from '../se3'; +import * as linalg from './linalg'; +import {vec3} from './linalg'; + +type mat4 = number[]; +type vec4 = [number, number, number, number]; + +interface Orbit { + excentricity: number, + semimajorAxis: number, + inclination: number, + ascendingNodeLongitude: number, + periapsisArgument: number, + t0: number, + + lastE: number | undefined, + tf: number[] | undefined, +} + +interface Body { + position: vec3, + velocity: vec3, + orientation: vec4, + children: Body[], + mass: number, + orbit: Orbit, + spin: vec3, + name: string, +} + +export function updateBodyPhysics(time: number, body: Body, parentBody : Body | undefined = undefined) { + if (parentBody !== undefined) { + const mu = parentBody.mass; + const {position, velocity} = getCartesianState(body.orbit, mu, time); + body.position = [ + parentBody.position[0] + position[0], + parentBody.position[1] + position[1], + parentBody.position[2] + position[2], + ]; + body.velocity = [ + parentBody.velocity[0] + velocity[0], + parentBody.velocity[1] + velocity[1], + parentBody.velocity[2] + velocity[2], + ]; + } else { + body.position = [0, 0, 0]; + body.velocity = [0, 0, 0]; + } + body.orientation = getOrientation(body, time); + + if (body.children !== undefined) { + for (const child of body.children) { + updateBodyPhysics(time, child, body); + } + } +} + +export function findSoi(rootBody: Body, position: number[]) : Body { + const bodies = [rootBody]; + let body : Body; + while (bodies.length > 0) { + body = bodies.shift()!; + + if (body.children === undefined) { + return body; + } + + for (const child of body.children) { + const soi = child.orbit.semimajorAxis * Math.pow(child.mass / body.mass, 2/5); + const pos = position; + const bod = child.position; + const dr = [pos[0] - bod[0], pos[1] - bod[1], pos[2] - bod[2]]; + if (dr[0]**2 + dr[1]**2 + dr[2]**2 < soi**2) { + bodies.push(child); + } + } + } + + return body!; +} + +export function computeOrbit(player: any, body: Body, time: number) { + const {cross, diff, norm, dot, scale} = linalg; + const rvec = diff(player.position, body.position); + const r = norm(rvec); + + if (norm(player.velocity) < 1e-6) { + // cheating + console.log('cheating'); + player.velocity = scale(cross([1, 1, 1], rvec), 0.01/(r**2)); + } + const vvec = diff(player.velocity, body.velocity); + const v = norm(vvec); + const Hvec = cross(rvec, vvec); + const H = norm(Hvec); + + const mu = body.mass; + const p = H**2 / mu; + const resinnu = Math.sqrt(p/mu) * dot(vvec, rvec) + const recosnu = p - r; + + const e = Math.sqrt(resinnu**2 + recosnu**2) / r; + + // should also work for hyperbolic orbits + const a = p/(1-e**2); + + const x = scale(rvec, 1/r); + const yy = cross(Hvec, rvec); + const y = scale(yy, 1/norm(yy)); + const z = scale(Hvec, 1/H); + + // Om i and w can be skipped when we just give tf... + let Om = Math.atan2(Hvec[0], -Hvec[1]); + if (Hvec[0] === 0 && Hvec[1] === 0) { + Om = 0; + } + + let i = Math.atan2(Math.sqrt(Hvec[0]**2 + Hvec[1]**2), Hvec[2]); + if (i * Math.sign((Hvec[0] || 1) * Math.sin(Om)) < 0) { + i *= -1; + } + + const nu = Math.atan2(resinnu, recosnu); + + let w = Math.atan2(rvec[2] / r / Math.sin(i), y[2] / Math.sin(i)) || 0 - nu; + + let t0; + let E; + + if (a < 0) { + E = 2 * Math.atanh(Math.sqrt((e-1)/(e+1)) * Math.tan(nu/2)); + const n = Math.sqrt(-mu / (a**3)); + t0 = time - (e*Math.sinh(E) - E) / n; + } else { + E = 2 * Math.atan(Math.sqrt((1-e)/(1+e)) * Math.tan(nu/2)); + const n = Math.sqrt(mu / (a**3)); + t0 = time - (1/n)*(E - e*Math.sin(E)); + } + + // column-major... see se3.js + const tf = se3.product([ + x[0], x[1], x[2], 0, + y[0], y[1], y[2], 0, + z[0], z[1], z[2], 0, + 0, 0, 0, 1, + ], se3.rotz(-nu)); + + const orbit = { + excentricity: e, + semimajorAxis: a, + inclination: i, + ascendingNodeLongitude: Om, + periapsisArgument: w, + t0, + tf, + lastE: E, + }; + + return orbit; +} + +/** Let's be honest I should clean this up. + * + * This is the part that solves Kepler's equation using Newton's method. + * For circular-ish orbits, one or two iterations are usually enough. + * More excentric orbits can take more (6 or 7?). + * + * For near-parabolic orbits (and some others?) it often fails to converge... + */ +export function getCartesianState(orbit: Orbit, mu: number, time: number) { + const { + excentricity: e, + semimajorAxis: a, + inclination: i, + ascendingNodeLongitude: Om, + periapsisArgument: w, + t0, + } = orbit; + let n = Math.sqrt(mu/(a**3)); + if (a < 0) { + n = Math.sqrt(mu/-(a**3)); // mean motion + } + const M = n * (time - t0); // mean anomaly + + // Newton's method + var E2 = 0; + var E = orbit.lastE || M; + let iterations = 0; + // a clever guess? https://link.springer.com/article/10.1023/A:1008200607490 + // doesn't work at all. + + while (Math.abs(E - E2) > 1e-10) { + if (e < 0.001) { + break; + } + E = E2; + if (e < 1) { + E2 = E - (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); + } else if (e > 1) { + E2 = E - (-E + e * Math.sinh(E) - M) / (e * Math.cosh(E) - 1); + } else { + E2 = E - (E + E*E*E/3 - M) / (1 + E*E); + } + + iterations++; + if (iterations > 100) { + console.log('numerical instability'); + return {}; + } + } + orbit.lastE = E; + let nu; + if (e > 1) { + nu = 2 * Math.atan(Math.sqrt((e+1) / (e-1)) * Math.tanh(E/2)); + } else { + nu = 2 * Math.atan(Math.sqrt((1+e) / (1-e)) * Math.tan(E/2)); + } + const p = a * (1 - e**2); + const r = p / (1 + e * Math.cos(nu));// * ((a < 0) ? -1 : 1); + const rd = e * Math.sqrt(mu / p) * Math.sin(nu); + + if (orbit.tf === undefined) { + // FIXME: this is actually borken. :/ + orbit.tf = [se3.rotz(Om), se3.rotx(i), se3.rotz(w)].reduce(se3.product); + } + + const tf = se3.product(orbit.tf, se3.rotz(nu)); + const pos = se3.apply(tf, [r, 0, 0, 1]); + const vel = se3.apply(tf, [rd, Math.sqrt(p * mu) / r, 0, 1]); + + return { + position: pos.slice(0, 3), + velocity: vel.slice(0, 3), + }; +} + +function getOrientation(body: Body, time: number) { + return se3.rotxyz( + body.spin[0] * time, + body.spin[1] * time, + body.spin[2] * time, + ); +} + +export function makeOrbitObject(gl: WebGLRenderingContext, glContext: any, orbit: Orbit, parentPosition: number[]) { + const position = parentPosition; + + // FIXME: currently borken. + // const orientation = [ + // se3.rotz(orbit.ascendingNodeLongitude), + // se3.rotx(orbit.inclination), + // se3.rotz(orbit.periapsisArgument), + // ].reduce(se3.product); + const orientation = orbit.tf; + + const buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + + const a = orbit.semimajorAxis; + const b = a * Math.sqrt(1 - orbit.excentricity**2); + + const x = - orbit.semimajorAxis * orbit.excentricity; + + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + x-a, -b, 0, -1, -1, + x-a, +b, 0, -1, +1, + x+a, -b, 0, +1, -1, + x+a, +b, 0, +1, +1, + ]), gl.STATIC_DRAW); + + const geometry = { + glBuffer: buffer, + numVertices: 4, + delete: () => gl.deleteBuffer(buffer), + }; + + return { + geometry, + orientation, + position, + glContext, + }; +} \ No newline at end of file diff --git a/skycraft/package.json b/skycraft/package.json index 7325199..f387703 100644 --- a/skycraft/package.json +++ b/skycraft/package.json @@ -1,14 +1,14 @@ { "name": "skycraft", "version": "1.0.0", - "main": "index.js", + "main": "index.ts", "license": "MIT", "dependencies": { "esbuild": "^0.14.2" }, "scripts": { - "watch": "esbuild --outfile=app.js index.js --bundle --sourcemap=inline --watch", - "serve": "esbuild --outfile=app.js index.js --bundle --sourcemap=inline --servedir=.", - "build": "esbuild --outfile=app.js index.js --bundle --minify" + "watch": "esbuild --outfile=app.js index.ts --bundle --sourcemap=inline --watch", + "serve": "esbuild --outfile=app.js index.ts --bundle --sourcemap=inline --servedir=.", + "build": "esbuild --outfile=app.js index.ts --bundle --minify" } } diff --git a/skycraft/stl.js b/skycraft/stl.ts similarity index 100% rename from skycraft/stl.js rename to skycraft/stl.ts diff --git a/skycraft/tsconfig.json b/skycraft/tsconfig.json new file mode 100644 index 0000000..dd885af --- /dev/null +++ b/skycraft/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "lib": ["es2022", "dom"], + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true + }, + "exclude": [ + "node_modules", + "dist" + ] + } \ No newline at end of file