diff --git a/skycraft/index.js b/skycraft/index.js index 52ae1c1..f4731b8 100644 --- a/skycraft/index.js +++ b/skycraft/index.js @@ -3,43 +3,8 @@ import * as se3 from '../se3'; import {loadTexture, makeProgram} from '../gl'; import {makeFace, makeBufferFromFaces} from '../geometry'; - -const linalg = { - cross: (a, b) => { - return [ - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0], - ]; - }, - diff: (a, b) => { - return [ - a[0] - b[0], - a[1] - b[1], - a[2] - b[2], - ]; - }, - add: (a, b) => { - return [ - a[0] + b[0], - a[1] + b[1], - a[2] + b[2], - ]; - }, - norm: a => { - return Math.sqrt(a[0]**2 + a[1]**2 + a[2]**2); - }, - dot: (a, b) => { - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - }, - scale: (a, s) => { - return [ - a[0] * s, - a[1] * s, - a[2] * s, - ]; - }, -}; +import { loadStlModel } from './stl'; +import * as linalg from './linalg'; const VSHADER = ` attribute vec3 aPosition; @@ -462,7 +427,7 @@ function closeToPlanet(context) { } function makeSun(seed) { - const radius = 79; + const radius = 7; const radiusChunks = Math.floor(radius / CHUNKSIZE); const chunks = []; @@ -756,6 +721,9 @@ function handleInput(context) { case 'KeyD': move(0.0, 0.5); return; + case 'KeyR': + context.timeOffset += 1; + return; } }); } @@ -998,6 +966,7 @@ function getCartesianState(orbit, mu, time) { 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); } @@ -1023,11 +992,14 @@ function makeOrbitObject(context, orbit, parentPosition) { const {gl} = context; const position = parentPosition; const glContext = context.orbitGlContext; - const orientation = [ - se3.rotz(orbit.ascendingNodeLongitude), - se3.rotx(orbit.inclination), - se3.rotz(orbit.periapsisArgument), - ].reduce(se3.product); + 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); @@ -1062,7 +1034,7 @@ const kGravitationalConstant = 6.674e-11; function getObjects(context, body, parentPosition) { const objects = []; - const {gl, glContext} = context; + const {gl, glContext, player} = context; const {position, orientation} = body; if (body.glBuffer === undefined) { @@ -1078,6 +1050,19 @@ function getObjects(context, body, parentPosition) { if (parentPosition !== undefined) { const orbitObject = makeOrbitObject(context, body.orbit, parentPosition); objects.push(orbitObject); + } else { + const shipOrientation = [ + se3.rotationOnly(player.tf), + se3.rotationOnly(context.camera.tf), + se3.rotxyz(-Math.PI / 2, 0, Math.PI / 2), + ].reduce(se3.product); + const shipPos = player.position; + objects.push({ + geometry: makeBufferFromFaces(gl, context.spaceship), + orientation: shipOrientation, + position: shipPos, + glContext, + }); } if (body.children !== undefined) { @@ -1108,7 +1093,12 @@ function draw(context) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - const viewMatrix = se3.inverse(se3.product(context.player.tf, context.camera.tf)); + const viewMatrix = se3.inverse([ + context.player.tf, // player position & orientation + + context.camera.tf, // camera orientation relative to player + se3.translation(0, 1, 4), // step back from the player + ].reduce(se3.product)); let lastGlContext; for (const {position, orientation, geometry, glContext} of objects) { @@ -1135,7 +1125,8 @@ function draw(context) { function tick(time, context) { handleInput(context); - updatePhysics(time * 0.001, context); + const simTime = time * 0.001 + context.timeOffset; + updatePhysics(simTime, context); const campos = context.player.position; @@ -1198,6 +1189,15 @@ async function main() { return; } + // TODO + // [ ] loading bar + // [ ] spaceship + // [ ] landing + // [ ] huge planets + // [ ] lighting + + const modelPromise = loadStlModel('spaceship.stl'); + const context = { gl, projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 10000.0), @@ -1213,7 +1213,7 @@ async function main() { keys: new Set(), lightDirection: [-0.2, -0.5, 0.4], skyColor: [0.10, 0.15, 0.2], - ambiantLight: 0.7, + ambiantLight: 0.4, blockSelectDistance: 8, flying: true, isOnGround: false, @@ -1221,6 +1221,7 @@ async function main() { jumpForce: 6.5, // objects: makeObjects(gl), universe: getSolarSystem(0), + timeOffset: 0, }; context.glContext = await initWorldGl(gl); @@ -1228,6 +1229,10 @@ async function main() { initUiListeners(canvas, context); // setupParamPanel(context); + const starshipGeom = await modelPromise; + console.log(`loaded ${starshipGeom.length} triangles`); + + context.spaceship = starshipGeom; requestAnimationFrame(time => tick(time, context)); } diff --git a/skycraft/linalg.js b/skycraft/linalg.js new file mode 100644 index 0000000..0dfae12 --- /dev/null +++ b/skycraft/linalg.js @@ -0,0 +1,38 @@ +export function cross(a, b) { + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ]; +} + +export function diff(a, b) { + return [ + a[0] - b[0], + a[1] - b[1], + a[2] - b[2], + ]; +} + +export function add(a, b) { + return [ + a[0] + b[0], + a[1] + b[1], + a[2] + b[2], + ]; +} + +export function norm(a) { + return Math.sqrt(a[0] ** 2 + a[1] ** 2 + a[2] ** 2); +} + +export function dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} +export function scale(a, s) { + return [ + a[0] * s, + a[1] * s, + a[2] * s, + ]; +} \ No newline at end of file diff --git a/skycraft/spaceship.stl b/skycraft/spaceship.stl new file mode 100644 index 0000000..a9a8783 Binary files /dev/null and b/skycraft/spaceship.stl differ diff --git a/skycraft/stl.js b/skycraft/stl.js new file mode 100644 index 0000000..e2167d5 --- /dev/null +++ b/skycraft/stl.js @@ -0,0 +1,76 @@ +import * as linalg from './linalg'; + +function parseTriangle(triangleData) { + const dv = new DataView(triangleData.buffer); + function data(idx) { + const offset = 4 * idx; + return dv.getFloat32(offset, /*littleEndian=*/true); + } + + const attributeByteCount = dv.getUint16(48, /*littleEndian=*/true); + console.assert(attributeByteCount === 0); + + const x = [data(3), data(4), data(5)]; + const y = [data(6), data(7), data(8)]; + const z = [data(9), data(10), data(11)]; + const n = linalg.cross(linalg.diff(y, x), linalg.diff(z, y)); + + return { + vertices: [x, y, z], + normals: new Array(3).fill(linalg.scale(n, 1 / linalg.norm(n))), + textures: new Array(3).fill([0, 0]), + }; // no normals, default texture +} + +export async function loadStlModel(url) { + const stlDataStream = (await fetch(url)).body; + const triangles = []; + + return new Promise((resolve, reject) => { + let bytesReceived = 0; + let gotHeader = false; + const partial = new Uint8Array(50); // each triangle is 50 bytes + let partialOffset = 0; + const reader = stlDataStream.getReader(); + + function pump() { + reader.read().then(({ done, value }) => { + if (done) { + return resolve(triangles); + } + + const skipped = bytesReceived; + bytesReceived += value.length; + let inputOffset = 0; + + if (!gotHeader) { + if (bytesReceived < 84) { // header + triangle count + return pump(); + } else { + gotHeader = true; + inputOffset = 84 - skipped; + } + } + + // parse triangle data + while (true) { + const spaceLeft = 50 - partialOffset; + console.assert(spaceLeft > 0); + + const leftToCopy = value.length - inputOffset; + const toCopy = value.subarray(inputOffset, inputOffset + Math.min(leftToCopy, spaceLeft)); + partial.set(toCopy, partialOffset); + if (leftToCopy < spaceLeft) { + break; + } + // parse a triangle! + triangles.push(parseTriangle(partial)); + partialOffset = 0; + inputOffset += toCopy.length; + } + pump(); + }); + } + pump(); + }); +} \ No newline at end of file