Skip to content

Instantly share code, notes, and snippets.

@CorvusCorax
Last active September 30, 2021 14:58
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save CorvusCorax/d6e6e98c946b8bfad56563b514df23a9 to your computer and use it in GitHub Desktop.
Save CorvusCorax/d6e6e98c946b8bfad56563b514df23a9 to your computer and use it in GitHub Desktop.
A Greasemonkey user script for SpaceX ISS SIM - useful to enable hard mode (no HUD) - optionally disable collissions, realistic orbital mechanics and other fun stuff
// ==UserScript==
// @name SpaceX ISS-SIM-Customizer
// @version 0.000016
// @author CorvusCorax
// @match https://iss-sim.spacex.com/
// @description A Greasemonkey user script for SpaceX ISS SIM - useful to enable hard mode (no HUD) - optionally disable collissions, realistic orbital mechanics and other fun stuff
// @include https://iss-sim.spacex.com/
// @license Creative Commons - CC BY 4.0
// ==/UserScript==
// add menu to configure iss-sim to settings DOM tree
var JQ=document.querySelector.bind(document)
JQ("#settings .modal-inner").innerHTML+='<div id="setting-tesla" class="setting active">TESLA:<br><span>HIDDEN EASTEREGG</span></div><div id="setting-space" class="setting active">SPACE:<br><span>RESTRICTED</span></div><div id="setting-hud" class="setting active">HUD:<br><span>ONLINE</span></div><div id="setting-limits" class="setting active">DOCKING CONSTRAINTS:<br><span>ORIGINAL SIM +-0.2deg</span></div><div id="setting-collision" class="setting active">COLLISIONS:<br><span>ENABLED</span></div><div id="setting-dock" class="setting active">ISS DOCKING PORT:<br><span>IDA-2</span></div><div id="setting-challenge" class="setting active">CHALLENGE:<br><span>DOCK</span></div>'
JQ("#hud").innerHTML+='<div id="rendezvous" class="hud-item" style="position: absolute; top: 15%; left: 15%; width: 110px; margin: 0; text-align: center; text-shadow: 0 0 10px #000; display: none;"><div class="label">RNDVZ</div><div id="rendezvous-phase" class="rate"></div><div id="rendezvous-time" class="rate"></div><div id="rendezvous-deltav" class="rate"></div><div id="rendezvous-deltat" class="rate"></div></div><div id="orbit" class="hud-item" style="position: absolute; top: 15%; right: 15%; width: 90px; margin: 0; text-align: center; text-shadow: 0 0 10px #000; display: none;"><div class="label">ORBIT</div><div id="orbit-vel" class="rate"></div><div id="orbit-alt" class="rate"></div><div id="orbit-apogee" class="rate"></div><div id="orbit-perigee" class="rate"></div><div id="orbit-period" class="rate"></div></div>'
JQ("#x-range .distance").style.width="100px"
JQ("#y-range .distance").style.width="100px"
JQ("#z-range .distance").style.width="100px"
{
var d=document.createElement('DIV')
d.id="propellant"
d.class="hud-item"
d.style="position: absolute; top: 15%; left: 5%; width: 100px; margin: 0; text-align: left; text-shadow: 0 0 12px #000; display: none; "
d.innerHTML='<div class="label">DELTA-V BUDGET</div><div id="prop-used" class="rate"></div><div id="prop-left" class="rate"></div><div class="label">MAIN BURN (M)</div><div id="burn-onoff" class="rate">OFF</div><div class="label">STATS</div><div id="stats-mission" class="rate"></div><div id="stats-real" class="rate"></div><div id="stats-fps" class="rate"></div>'
JQ("#hud").after(d)
}
// The main functionality override must be loaded AFTER ISS-SIM has finished loading.
function GMInitializer() {
// allow realistic orbital mechanics
window.GMisOrbitalMechanics = 1
// override gravity toggler function
// Bug: Tampermonkey extension for firefox does not support overriding functions already bound to event listeners
// Fix: Unbind event listener, bind our own instead
$("#setting-gravity").removeEventListener("click", toggleGravity, !1)
$("#setting-gravity").removeEventListener("touchstart", toggleGravity, !1)
var GMAmbientLight = new THREE.AmbientLight(0xffffff, 0.1);
toggleGravity = function() {
if (!GMisChallenge || GMisTrajectoryVisible) return // cannot switch of gravity when trying rendezvous manouver or showing trajectory
GMisOrbitalMechanics = GMisOrbitalMechanics<3 ? GMisOrbitalMechanics+1 : 1
switch (GMisOrbitalMechanics) {
case 1:
(isGravity = !0, $("#setting-gravity").classList.add("active"), $("#setting-gravity span").innerHTML = "ON")
break;
case 2:
(isGravity = !1, $("#setting-gravity span").innerHTML = "PROPER ORBITAL MECHANICS")
$("#setting-gravity").classList.add("active")
$("#orbit").style.display="block"
$("#rendezvous").style.display="block"
// Have earth the correct way around, and ISS in correct orbit
earthObject.rotation.fromArray([0,0,(-90+51) * Math.PI/180.0])
camera.children.push(GMAmbientLight)
GMSpeedMeasure=0 // force end speed measurement - fluctuating deltaT messes up orbits
break;
case 3:
($("#setting-gravity").classList.remove("active"), $("#setting-gravity span").innerHTML = "OFF")
$("#orbit").style.display="none"
$("#rendezvous").style.display="none"
// Revert back to ISS-SIM reverse earth
earthObject.rotation.fromArray([0,0,51 * Math.PI/180.0])
starsObject.rotation.fromArray([0,0,0])
lightISS_Primary.position.set(0, 4e10, 4e10)
camera.children.pop(GMAmbientLight)
issObject.children[3].children[0].children[0].children[6].children[8].children[6].children[6].children[3].rotation.x=60*Math.PI/180.0
issObject.children[3].children[0].children[0].children[6].children[8].children[7].children[6].children[3].rotation.x=60*Math.PI/180.0
break;
}
}
$("#setting-gravity").addEventListener("click", toggleGravity, !1)
$("#setting-gravity").addEventListener("touchstart", toggleGravity, !1)
// unhide the Tesla
window.GMisTeslaHidden = 1
function GMToggleTeslaHidden() {
GMisTeslaHidden ? (GMisTeslaHidden = !1, $("#setting-tesla span").innerHTML = "VISIBLE") : (GMisTeslaHidden = !0, $("#setting-tesla span").innerHTML = "HIDDEN EASTEREGG")
}
$("#setting-tesla").addEventListener("click", GMToggleTeslaHidden, !1)
$("#setting-tesla").addEventListener("touchstart", GMToggleTeslaHidden, !1)
// unrestrict space
window.GMisSpaceRestricted = 1
function GMToggleSpaceRestricted() {
GMisSpaceRestricted ? (GMisSpaceRestricted = !1, $("#setting-space span").innerHTML = "INFINITE") : (GMisChallenge && ( GMisSpaceRestricted = !0, $("#setting-space span").innerHTML = "RESTRICTED"))
}
$("#setting-space").addEventListener("click", GMToggleSpaceRestricted, !1)
$("#setting-space").addEventListener("touchstart", GMToggleSpaceRestricted, !1)
// HUD visible
window.GMHUDStatus = 1
window.GMisHUDVisible = 1
window.GMisTrajectoryVisible = 0
function GMshowHUD() {
issObject.children[4].visible=true
issObject.children[5].visible=true
issObject.children[6].visible=true
issObject.children[7].visible=true
$("#hud").style.display="block"
$("#hud-tips").style.display="block"
$("#hud-worms").style.display="block"
}
function GMhideHUD() {
issObject.children[4].visible=false
issObject.children[5].visible=false
issObject.children[6].visible=false
issObject.children[7].visible=false
$("#hud").style.display="none"
$("#hud-tips").style.display="none"
$("#hud-worms").style.display="none"
}
function GMToggleHUDVisible() {
GMHUDStatus = GMHUDStatus<3 ? GMHUDStatus+1 : 1
switch (GMHUDStatus) {
case 1:
GMisHUDVisible = !0, $("#setting-hud").classList.add("active"), $("#setting-hud span").innerHTML = "ONLINE", GMshowHUD()
break;
case 2:
( GMisOrbitalMechanics==2 || ( GMisOrbitalMechanics=1, toggleGravity() ) ), GMisTrajectoryVisible = !0, $("#setting-hud span").innerHTML = "ONLINE + TRAJECTORY"
break;
case 3:
GMisHUDVisible = !1, GMisTrajectoryVisible = !1, $("#setting-hud").classList.remove("active"), $("#setting-hud span").innerHTML = "OFFLINE", GMhideHUD()
break;
}
}
$("#setting-hud").addEventListener("click", GMToggleHUDVisible, !1)
$("#setting-hud").addEventListener("touchstart", GMToggleHUDVisible, !1)
// relaxed limits
window.GMLimits = 1
window.GMisFuelLimit = !1
window.GMisLimitsOriginal = !0
function GMToggleLimits() {
GMLimits = GMLimits < 4 ? GMLimits+1 : 1
switch (GMLimits) {
case 1:
GMisLimitsOriginal = !0, $("#setting-limits span").innerHTML = "ORIGINAL SIM +-0.2deg"
GMisFuelLimit = !1
break;
case 2:
GMisLimitsOriginal = !1, $("#setting-limits span").innerHTML = "IDA SPECS +-4.0deg 0.1m/s"
GMisFuelLimit = !1
break;
case 3:
GMisLimitsOriginal = !0, $("#setting-limits span").innerHTML = "LIMITED PROPELLANT, ORIGINAL SIM +-0.2deg"
GMisFuelLimit = !0
break;
case 4:
GMisLimitsOriginal = !1, $("#setting-limits span").innerHTML = "LIMITED PROPELLANT, IDA SPECS +-4.0deg 0.1m/s"
GMisFuelLimit = !0
break;
}
}
$("#setting-limits").addEventListener("click", GMToggleLimits, !1)
$("#setting-limits").addEventListener("touchstart", GMToggleLimits, !1)
// collision
window.GMisCollisions = 1
function GMToggleCollisions() {
GMisCollisions ? (GMisCollisions = !1, $("#setting-collision").classList.remove("active"), $("#setting-collision span").innerHTML = "DISABLED") : (GMisCollisions = !0, $("#setting-collision").classList.add("active"), $("#setting-collision span").innerHTML = "ENABLED" )
}
$("#setting-collision").addEventListener("click", GMToggleCollisions, !1)
$("#setting-collision").addEventListener("touchstart", GMToggleCollisions, !1)
// docking ports
window.GMisDocking = 1
var GMRotForward = new THREE.Object3D()
GMRotForward.position.fromArray([0,0,-0.1])
var GMRotPort = new THREE.Object3D()
GMRotPort.rotateY(0.5*Math.PI)
GMRotPort.position.fromArray([-0.1,0,0])
var GMRotAft = new THREE.Object3D()
GMRotAft.rotateY(Math.PI)
GMRotAft.position.fromArray([0,0,0.1])
var GMRotZenithUS = new THREE.Object3D()
GMRotZenithUS.rotateX(-0.5*Math.PI)
GMRotZenithUS.position.fromArray([0,-0.1,0])
var GMRotNadirUS = new THREE.Object3D()
GMRotNadirUS.rotateX(0.5*Math.PI)
GMRotNadirUS.position.fromArray([0,0.1,0])
var GMRotZenithRU = GMRotAft.clone()
GMRotZenithRU.rotateX(-0.5*Math.PI)
GMRotZenithRU.position.fromArray([0,-0.1,0])
var GMRotNadirRU = GMRotAft.clone()
GMRotNadirRU.rotateX(0.5*Math.PI)
GMRotNadirRU.position.fromArray([0,0.1,0])
var GMPosIDA2 = new THREE.Vector3(0,0,0)
var GMPosHarmonyZt = new THREE.Vector3(0,3.5,-4.75)
var GMPosHarmonyNd = new THREE.Vector3(0,-2.1,-4.75)
var GMPosPMA3 = new THREE.Vector3(12,0,-21)
var GMPosZvezda = new THREE.Vector3(0,1.5,-53.9)
var GMPosPoisk = new THREE.Vector3(0,7.0,-40.95)
var GMPosPirs = new THREE.Vector3(0,-4.1,-40.95)
var GMPosRassvet = new THREE.Vector3(0,-6.5,-28.0)
var GMIssRestoreOffset = GMPosIDA2.clone()
var GMIssApproach = GMRotForward.clone()
function GMRestoreISSPos() {
issObject.children[0].position.add(GMIssRestoreOffset)
issObject.children[1].position.add(GMIssRestoreOffset)
issObject.children[2].position.add(GMIssRestoreOffset)
issObject.children[3].position.add(GMIssRestoreOffset)
}
function GMSetISSPos() {
var GMnegRestore = GMIssRestoreOffset.clone()
GMnegRestore.negate()
issObject.children[0].position.add(GMnegRestore)
issObject.children[1].position.add(GMnegRestore)
issObject.children[2].position.add(GMnegRestore)
issObject.children[3].position.add(GMnegRestore)
}
function GMRingsFront() {
issObject.children[4].position.fromArray([0,0,20])
issObject.children[5].position.fromArray([0,0,40])
issObject.children[6].position.fromArray([0,0,80])
issObject.children[4].rotation.fromArray([0,0,0])
issObject.children[5].rotation.fromArray([0,0,0])
issObject.children[6].rotation.fromArray([0,0,0])
}
function GMRingsAft() {
issObject.children[4].position.fromArray([0,0,-20])
issObject.children[5].position.fromArray([0,0,-40])
issObject.children[6].position.fromArray([0,0,-80])
issObject.children[4].rotation.fromArray([0,Math.PI,0])
issObject.children[5].rotation.fromArray([0,Math.PI,0])
issObject.children[6].rotation.fromArray([0,Math.PI,0])
}
function GMRingsZenith() {
issObject.children[4].position.fromArray([0,20,0])
issObject.children[5].position.fromArray([0,40,0])
issObject.children[6].position.fromArray([0,80,0])
issObject.children[4].rotation.fromArray([-0.5*Math.PI,0,0])
issObject.children[5].rotation.fromArray([-0.5*Math.PI,0,0])
issObject.children[6].rotation.fromArray([-0.5*Math.PI,0,0])
}
function GMRingsNadir() {
issObject.children[4].position.fromArray([0,-20,0])
issObject.children[5].position.fromArray([0,-40,0])
issObject.children[6].position.fromArray([0,-80,0])
issObject.children[4].rotation.fromArray([0.5*Math.PI,0,0])
issObject.children[5].rotation.fromArray([0.5*Math.PI,0,0])
issObject.children[6].rotation.fromArray([0.5*Math.PI,0,0])
}
function GMToggleDocking() {
GMRestoreISSPos()
GMisDocking = GMisDocking<8 ? GMisDocking+1 : 1
var name = ""
switch (GMisDocking) {
case 1:
GMIssRestoreOffset.copy(GMPosIDA2)
GMIssApproach.copy(GMRotForward)
name = "IDA-2"
GMRingsFront()
break;
case 2:
GMIssRestoreOffset.copy(GMPosHarmonyZt)
GMIssApproach.copy(GMRotZenithUS)
name = "HARMONY ZENITH"
GMRingsZenith()
break;
case 3:
GMIssRestoreOffset.copy(GMPosHarmonyNd)
GMIssApproach.copy(GMRotNadirUS)
name = "HARMONY NADIR"
GMRingsNadir()
break;
case 4:
GMIssRestoreOffset.copy(GMPosPMA3)
GMIssApproach.copy(GMRotPort)
name = "PMA3 ( WATCH RADIATOR! )"
GMRingsNadir()
issObject.children[4].position.x=5
issObject.children[5].position.x=5
issObject.children[6].position.x=5
// rotate radiator to allow approach
issObject.children[3].children[0].children[0].children[6].children[8].children[7].children[5].rotateY(-Math.PI/2)
break;
case 5:
GMIssRestoreOffset.copy(GMPosZvezda)
GMIssApproach.copy(GMRotAft)
name = "ZVEZDA"
GMRingsAft()
issObject.children[3].children[0].children[0].children[6].children[8].children[7].children[5].rotateY(-Math.PI/2)
break;
case 6:
GMIssRestoreOffset.copy(GMPosPoisk)
GMIssApproach.copy(GMRotZenithRU)
name = "POISK"
GMRingsZenith()
break;
case 7:
GMIssRestoreOffset.copy(GMPosPirs)
GMIssApproach.copy(GMRotNadirRU)
name = "PIRS"
GMRingsNadir()
break;
case 8:
GMIssRestoreOffset.copy(GMPosRassvet)
GMIssApproach.copy(GMRotNadirRU)
name = "RASSVET"
GMRingsNadir()
break;
}
GMSetISSPos()
$("#setting-dock span").innerHTML = name
}
$("#setting-dock").addEventListener("click", GMToggleDocking, !1)
$("#setting-dock").addEventListener("touchstart", GMToggleDocking, !1)
// challenge
window.GMisChallenge = 1
$("#option-restart").removeEventListener("click", resetPosition, !1)
$("#option-restart").removeEventListener("touchstart", resetPosition, !1)
window.GMPhase = 127
window.GMPerigee = 190
window.GMApogee = 205
window.GMInclination = Math.PI*1000 // 3000 odd meters ;)
resetPosition = function() {
var px=12,
py=30,
pz=50,
vx=0,
vy=0,
vz=0
if (!GMisChallenge) {
var phi = new THREE.Euler(-GMPhase*Math.PI/180,0,0),
r = GMGravBodyRadius+(GMPerigee*1000),
rref = GMGravBodyCenter.length(),
a = GMGravBodyRadius+((GMPerigee+GMApogee)*500),
pos = new THREE.Vector3(0,0,0).sub(GMGravBodyCenter).applyEuler(phi).setLength(r).add(GMGravBodyCenter),
v = Math.sqrt(GMG*GMMe*((2.0/r)-(1.0/a))),
omega = Math.sqrt((GMG*GMMe)/rref)/rref,
vel = new THREE.Vector3(0,0,v).applyEuler(phi).sub(GMCalcPseudoVelocity(pos,omega)).multiplyScalar(GMFixedStepTime)
px=pos.x + GMInclination // this adds enough challenge by screwing everything just a little bit up
py=pos.y
pz=pos.z
vx=vel.x
vy=vel.y
vz=vel.z
}
isGameOver=!0, resetMovement(), gsap.to(motionVector, 5, {
x: vx,
y: vy,
z: vz,
ease: "expo.out"
}), gsap.to(translationVector, 5, {
x: 0,
y: 0,
z: 0,
ease: "expo.out"
}), gsap.to(camera.position, 5, {
x: px,
y: py,
z: pz,
ease: "expo.out"
}), gsap.to(camera.rotation, 5, {
x: -20 * toRAD,
y: -10 * toRAD,
z: 15 * toRAD,
ease: "expo.out",
onComplete: GMGameStart
})
}
function GMToggleChallenge() {
GMisChallenge ? ((GMisSpaceRestricted && GMToggleSpaceRestricted()), ( GMisOrbitalMechanics==2 || ( GMisOrbitalMechanics=1, toggleGravity() ) ), GMisChallenge = !1, $("#setting-challenge span").innerHTML = "RENDEZVOUS + DOCK", GMDeltaVBudget=175, resetPosition()) : (GMisChallenge = !0, $("#setting-challenge span").innerHTML = "DOCK", GMDeltaVBudget=25, resetPosition() )
}
$("#option-restart").addEventListener("click", resetPosition, !1)
$("#option-restart").addEventListener("touchstart", resetPosition, !1)
$("#setting-challenge").addEventListener("click", GMToggleChallenge, !1)
$("#setting-challenge").addEventListener("touchstart", GMToggleChallenge, !1)
// lots of functionality here for proper orbital mechanics - start with globals
var GMCameraRotationHelper = new THREE.Object3D()
var GMoldTime = 0
window.GMFixedStepTime = 0.05
window.GMCurrentFrameTime = 0.05
window.GMMissionTimer = 0
window.GMGameTimer = 0
// fix motion - ISS-SIM keeps all velocities in a per frame format, even though frame timing is varying
// let's fix that. This function undo's all motion computations and reapply's them with correct timing
// the alternative would be to reimplement the render() function.
function GMfixMotion() {
var k = GMCurrentFrameTime/GMFixedStepTime
// fix translation
!isGameOver && isGravity && (camera.position.y = camera.position.y + gravity)
camera.position.sub(motionVector)
correctedMotion = motionVector.clone().multiplyScalar(k)
camera.position.add(correctedMotion)
!isGameOver && isGravity && (camera.position.y = camera.position.y - (gravity*k))
// fix rotation
camera.rotateZ(-currentRotationZ)
camera.rotateY(-currentRotationY)
camera.rotateX(-currentRotationX)
camera.rotateX(currentRotationX*k)
camera.rotateY(currentRotationY*k)
camera.rotateZ(currentRotationZ*k)
}
window.GMDeltaVUsed = 0
window.GMDeltaVBudget = 25
var GMOldMotionVector = new THREE.Vector3()
var GMOldRotation = new THREE.Vector3()
// calculate gravity - for proper orbital mechanics - needs coordinate system translation
// due to rotating coordinate reference frame used by ISS-SIM
window.GMGravBodyCenter = new THREE.Vector3(0,-6821000 ,0) // hardcode in case earth object is not spawned yet 450 km orbit
window.GMGravBodyRadius = 6371000
window.GMGravBodyAtmosphereRadius = GMGravBodyRadius + 80000
window.GMG = 6.6743015e-11
window.GMMe = 5.9722e+24
// var GMMe = 5.9722e+28 // black hole earth to test orbital mechanics
window.GMIssOrbitAxis = new THREE.Vector3(1,0,0)
// energy is sum of gravitational portential and kinematic
function GMOrbitEnergy(v,p) {
var r = Math.abs(p.distanceTo(GMGravBodyCenter)),
eP = -GMG*GMMe / r,
eV = 0.5 * (v.length()**2)
return eP + eV
}
// function to calculate pseudo velocity coming from rotating reference frame at any point
function GMCalcPseudoVelocity(position,omega) {
var pseudoCenter = GMGravBodyCenter.clone()
// frame rotates around x axis - this makes this easy
pseudoCenter.x = position.x
var radius = Math.abs(position.distanceTo(pseudoCenter))
return pseudoCenter.clone().sub(position).normalize().cross(GMIssOrbitAxis).multiplyScalar(radius*omega)
}
// function to calculate local gravity vector
function GMCalcLocalGravityVector(position) {
var r = Math.abs(position.distanceTo(GMGravBodyCenter))
var grav = GMG*GMMe / (r**2)
var earthVector = GMGravBodyCenter.clone().sub(position).normalize()
return earthVector.clone().multiplyScalar(grav)
}
function GMprintTime(time) {
var res=""
if (time<0) {
res="-"
time=-time
}
res += Math.floor(time/60).toString().padStart(2,"0")
res += ":"
res += Math.floor(time%60).toString().padStart(2,"0")
return res
}
// this function calculates the orbital mechanics creates in a rotating reference frame
// for that we compute a single motion step at orbital velocity with earth gravity acting,
// then transform it back into the rotating ref frame
function GMOrbitalMotion() {
var mu = GMG*GMMe
var GMIssAngularVelocity = Math.sqrt(mu/GMGravBodyCenter.length())/GMGravBodyCenter.length()
// calculating a motion step in rotating reference frame is now in its own class
// - warning hack: in every time step motionVector is added to camera
// but we prefer doing that ourselves so we substract motionVector now so the renderer can re-add it
var p = camera.position.clone().sub(motionVector.clone().multiplyScalar(GMCurrentFrameTime/GMFixedStepTime))
var o = new GMOrbiter(p, motionVector.clone().multiplyScalar(1.0/GMFixedStepTime))
o.advance(GMCurrentFrameTime)
// copy new velocity into motion vector
motionVector.copy(o.v).multiplyScalar(GMFixedStepTime)
// apply new position to camera
camera.position.copy(o.p)
// calculate some orbital parameters for display
var r = Math.abs(camera.position.distanceTo(GMGravBodyCenter))
var v = o.vs.length()
var gammacos = o.vs.clone().normalize().dot(camera.position.clone().sub(GMGravBodyCenter).normalize())
var gamma = Math.acos(gammacos)
var gammasin = Math.sin(gamma)
// semi major axis and eccentricity
var a = 1.0 / ( (2/r) - ((v**2) / mu) )
var e = Math.sqrt( (((r*(v**2)/mu)-1)**2)*gammasin**2 + gammacos**2 )
// periapis and apoapsis
var peri = a*(1-e), apo = a*(1+e)
// phase angle between ISS and Dragon - as seen from earth
var v1 = camera.position.clone().sub(GMGravBodyCenter).normalize()
var v2 = new THREE.Vector3(0,0,0).sub(GMGravBodyCenter).normalize()
var phase = v1.angleTo(v2)
if (v1.clone().cross(v2).dot(GMIssOrbitAxis)<0) phase=-phase
// a2 = r2 - circular ISS orbit
var a2 = GMGravBodyCenter.length()
// v3/a3/e3 hypothetical transfer orbit from current orbit (including gamma) to ISS - solve for DeltaV
var v3 = Math.sqrt(-2.0*a2*mu*((a2 - r)/(((r**3)*(gammasin**2)) - ((a2**2)*r))))
var a3 = 1.0 / ( (2/r) - ((v3**2) / mu) )
var e3 = Math.sqrt( (((r*(v3**2)/mu)-1)**2)*gammasin**2 + gammacos**2 )
// calculate true anomaly of transfer orbit
var phi = Math.acos(((a3*(1.0-e3**2))-r)/(e3*r))
if (gammacos<0) phi=-phi // correct for descending half orbit
// calculate time spent in transfer orbit - using Mean Anomaly M via eccentric anomaly E
var n = Math.sqrt(mu/a3**3)
var E = Math.acos((e3+Math.cos(phi))/(1.0+(e3*Math.cos(phi))))
var M = E-(e3*Math.sin(E))
if (gammacos<0) M=-M // once again correct for descending half
// calculate time of arrival - depends if we make a transfer up (destination at Apogee)
// or down (ISS is at perigee) and current Mean Anomaly
var rtime
if (a2>a3) {
rtime = Math.PI - M
} else {
rtime = (2.0*Math.PI) - M
}
if (rtime<0) rtime+=2.0*Math.PI
if (rtime>2.0*Math.PI) rtime-=2.0*Math.PI
rtime=rtime/n
// calculate periods
var T = 2.0 * Math.PI * Math.sqrt( a**3 / mu )
var T2 = 2.0 * Math.PI * Math.sqrt( a2**3 / mu )
// calculate phase after transfer
var pafter
if (a2>a3) {
pafter = phase+GMIssAngularVelocity*rtime-(Math.PI-phi)
} else {
pafter = phase+GMIssAngularVelocity*rtime-((2.0*Math.PI)-phi)
}
if (pafter < -Math.PI) pafter+=2.0*Math.PI
if (pafter > Math.PI) pafter-=2.0*Math.PI
// burn T=0 is when phase-after-transfer is zero. But rate at which phase is shrinking depends
// on current orbit - this timing is inaccurate, we don't take the current orbits Mean Anomaly into account
// but its "good enough" to give the hobby astronaut a good idea when to fire thrusters
var pdelta = 2.0*Math.PI*(T2-T)/T2
var btime = -T2*(pafter/pdelta)
$("#orbit-vel").innerHTML="Vel: "+v.toFixed(1)+" m/s"
$("#orbit-alt").innerHTML="Alt: "+((r-GMGravBodyRadius)/1000).toFixed(1)+" km"
$("#orbit-apogee").innerHTML="Ap: "+((apo-GMGravBodyRadius)/1000).toFixed(1)+" km"
$("#orbit-perigee").innerHTML="Pe: "+((peri-GMGravBodyRadius)/1000).toFixed(1)+" km"
$("#orbit-period").innerHTML="Prd: "+GMprintTime(T)
$("#rendezvous-phase").innerHTML="Phs: "+(phase*180.0/Math.PI).toFixed(1)+"°"
$("#rendezvous-time").innerHTML="Ht: "+GMprintTime(rtime)+"⇒"+(pafter*180.0/Math.PI).toFixed(1)+"°"
$("#rendezvous-deltav").innerHTML="Dv: "+((v3-v).toFixed(1))+" m/s"
$("#rendezvous-deltat").innerHTML="Dt: "+GMprintTime(btime)
// rotate earth more correctly
earthMesh.rotateY(-1e-4) // undo default rotation
earthMesh.rotateY(((2*Math.PI)/(24*60*60))*GMCurrentFrameTime) // and rotate eastwards (natural earth rotation)
starsObject.rotateX(-GMIssAngularVelocity*GMCurrentFrameTime)
earthObject.rotateOnWorldAxis(GMIssOrbitAxis,-GMIssAngularVelocity*GMCurrentFrameTime) // while perceived rotation is west
lightISS_Primary.position.applyEuler(new THREE.Euler(-GMIssAngularVelocity*GMCurrentFrameTime,0,0)) // rotate sun
// follow sun with ISS with solar panels
issObject.children[3].children[0].children[0].children[6].children[8].children[6].children[6].children[3].rotateX(-GMIssAngularVelocity*GMCurrentFrameTime)
issObject.children[3].children[0].children[0].children[6].children[8].children[7].children[6].children[3].rotateX(-GMIssAngularVelocity*GMCurrentFrameTime)
}
// helper class to accurately calculate orbital motion of any object in ISS rotating reference frame
class GMOrbiter {
constructor(p,v) {
this.p = (new THREE.Vector3()).copy(p)
this.v = (new THREE.Vector3()).copy(v)
this.vs = new THREE.Vector3()
}
clone() {
var c = new GMOrbiter(this.p,this.v)
c.vs.copy(this.vs)
return c
}
advance(t) {
var mu = GMG*GMMe
// velocity of rotating reference frame
var GMIssAngularVelocity = Math.sqrt(mu/GMGravBodyCenter.length())/GMGravBodyCenter.length()
var pseudoCamMotion = GMCalcPseudoVelocity(this.p,GMIssAngularVelocity)
// calculate local gravity vector and magnitude
var gravityVector = GMCalcLocalGravityVector(this.p)
// calculate current effective velocity vector and direction
var pseudoVelocity = this.v.clone().add(pseudoCamMotion)
// calculate orbital energy
var GMEnergy = GMOrbitEnergy(pseudoVelocity, this.p)
// calculate change in position and velocity in stationary coordinate system...
var pseudoNextVel1 = pseudoVelocity.clone().add(gravityVector.clone().multiplyScalar(t))
var averageVel1 = pseudoVelocity.clone().add(pseudoNextVel1).multiplyScalar(0.5)
var pseudoNextPos1 = this.p.clone().add(averageVel1.clone().multiplyScalar(t))
// iterative, 2 step for higher accuracy (that algo has a name, I forgot that name)
var gravityVector2 = GMCalcLocalGravityVector(pseudoNextPos1)
var averageGrav = gravityVector.clone().add(gravityVector2).multiplyScalar(0.5)
var pseudoNextVel = pseudoVelocity.clone().add(averageGrav.clone().multiplyScalar(t))
var averageVel2 = pseudoVelocity.clone().add(pseudoNextVel).multiplyScalar(0.5)
var pseudoNextPos = this.p.clone().add(averageVel2.clone().multiplyScalar(t))
// convert new position back into rotating coordinate frame at time t+1
this.p.copy(pseudoNextPos.clone().sub(GMGravBodyCenter).applyAxisAngle(GMIssOrbitAxis,-GMIssAngularVelocity*t).add(GMGravBodyCenter))
// same with velocity
var newPseudoMotion = GMCalcPseudoVelocity(this.p,GMIssAngularVelocity)
var newPseudoVelocity = pseudoNextVel.clone().applyAxisAngle(GMIssOrbitAxis,-GMIssAngularVelocity*t)
// correct orbital energy to prevent drifting
var velEnergyRequired = GMEnergy - (-mu / Math.abs(this.p.distanceTo(GMGravBodyCenter)))
var velocityRequired = Math.sqrt( 2.0*velEnergyRequired )
this.vs.copy(newPseudoVelocity.clone().normalize().multiplyScalar(velocityRequired))
this.v.copy(this.vs.clone().sub(newPseudoMotion))
}
}
window.GMTrajectoryStep=20.0
// Show expected trajectory
function GMShowTrajectory() {
var o = new GMOrbiter(camera.position, motionVector.clone().multiplyScalar(1.0/GMFixedStepTime))
var timestep = GMTrajectoryStep / (motionVector.length()/GMFixedStepTime) // calculate length of time iteration
var iters=1
if (!(timestep<1.0)) timestep=1.0; // prevent div by zero or extreme accuracy degradation
var ot=o.clone()
ot.advance(timestep)
var l=ot.clone().p.sub(o.p).length()
iters=Math.floor((GMTrajectoryStep/l)+0.5)
if (!(iters<10000)) { // prevent div by zero
GMTrajectoryObject.visible = false
return
}
if (!(iters<100)) iters=100; // prevent processing time explosions
for (var t=0; t<GMTrajectoryLength; t++) {
o.advance(timestep)
for (var t2=1;t2<iters;t2++) o.advance(timestep)
GMTrajectoryObject.children[t].position.copy(o.p)
}
}
// override some functions from ISS SIM with modified versions to implement this functionality
// start with some helper functions
function GMAddReason(a,b) {
return a != "" ? a + ", " + b : b
}
window.GMTimeLapse = 1
window.GMBurn=0
window.GMBurnTimer=0
checkCollision = function() {
// fix time stepping - do not allow big time jumps to ensure smoothness
var GMnewTime=Date.now()
if ( GMoldTime < GMnewTime - 100 ) {
GMoldTime = GMnewTime - 100
}
GMCurrentFrameTime = 0.001 * ( GMnewTime - GMoldTime )
GMoldTime=GMnewTime
GMfixMotion()
GMTrajectoryObject.visible = false
if (!0 !== isGameOver) {
// Main Burn incl. timer
if (GMBurn == 1){
GMBurnTimer+=GMCurrentFrameTime
$("#translate-backward-button").click()
}
$("#burn-onoff").innerHTML = (GMBurn == 1 ? '<span style="color:red">ON&nbsp;' + GMprintTime(GMBurnTimer)+"."+(GMBurnTimer%1.0).toFixed(2).toString().substring(2) + '</span>' : "OFF")
// timing
GMMissionTimer+=GMCurrentFrameTime
GMGameTimer+=GMCurrentFrameTime
$("#stats-mission").innerHTML="Mis: "+GMprintTime(GMMissionTimer)+"."+(GMMissionTimer%1.0).toFixed(2).toString().substring(2)
$("#stats-real").innerHTML="Sim: "+GMprintTime(GMGameTimer)+"."+(GMGameTimer%1.0).toFixed(2).toString().substring(2)
$("#stats-fps").innerHTML=(1.0/GMCurrentFrameTime).toFixed(1)+" fps"
// DeltaV update:
GMDeltaVUsed+=motionVector.clone().sub(GMOldMotionVector).length()/GMFixedStepTime
var rotationDeltaV=Math.abs(GMOldRotation.x-currentRotationX) + Math.abs(GMOldRotation.y-currentRotationY) + Math.abs(GMOldRotation.z-currentRotationZ)
GMDeltaVUsed+=0.02*rotationDeltaV*180/(Math.PI*moveSpeed)
$("#prop-used").innerHTML="Used: "+(GMDeltaVUsed*1.0).toFixed(1)+" m/s"
$("#prop-left").innerHTML="Budg: "+(GMDeltaVBudget-GMDeltaVUsed).toFixed(1)+" m/s"
$("#propellant").style.display="block"
// calculate gravity if applicable
if (GMisOrbitalMechanics==2) {
GMOrbitalMotion()
}
// time lapse
var realFrameTime = GMCurrentFrameTime
GMCurrentFrameTime = GMFixedStepTime // force k factor 1.0 for fast forward frames
var ti
for (ti=1; ti < GMTimeLapse; ti++) {
camera.position.add(motionVector)
!isGameOver && isGravity && (camera.position.y = camera.position.y - (gravity))
camera.rotateX(currentRotationX)
camera.rotateY(currentRotationY)
camera.rotateZ(currentRotationZ)
earthMesh.rotateY(1e-4)
GMMissionTimer+=GMCurrentFrameTime
// calculate gravity if applicable
if (GMisOrbitalMechanics==2) {
GMOrbitalMotion()
}
}
GMCurrentFrameTime = realFrameTime
// save motion vectors for deltaV update
GMOldMotionVector.copy(motionVector)
GMOldRotation.set(currentRotationX,currentRotationY,currentRotationZ)
if (GMisOrbitalMechanics==2 && GMisTrajectoryVisible) {
GMTrajectoryObject.visible = true
GMShowTrajectory()
}
var GMIssTarget = issObject.position.clone()
GMIssTarget.add(GMIssApproach.position)
GMCameraRotationHelper.quaternion.copy(camera.quaternion)
GMCameraRotationHelper.quaternion.multiply(GMIssApproach.quaternion.clone().inverse())
var e = hitRaycaster.intersectObjects(hitArray, !0),
t = Math.abs(camera.position.distanceTo(issObject.position)),
t2 = Math.abs(camera.position.distanceTo(GMIssTarget)),
a = Math.abs(GMCameraRotationHelper.rotation.x / toRAD),
r = Math.abs(GMCameraRotationHelper.rotation.y / toRAD),
o = Math.abs(GMCameraRotationHelper.rotation.z / toRAD),
i = Math.abs(motionVector.x),
n = Math.abs(motionVector.y),
s = Math.abs(motionVector.z),
ar = Math.sqrt(a*a+r*r),
rxy = Math.sqrt(currentRotationX*currentRotationX + currentRotationY*currentRotationY) / toRAD,
rz = Math.abs(currentRotationZ) / toRAD
if (hitDistance = t > .5 ? 1 : .1, hitRaycaster.far = hitDistance, updateRateColor(i >= .02 || n >= .02 || s >= .02 ? "warning" : i > toleranceRate || n > toleranceRate || s > toleranceRate ? "caution" : "normal"), GMisCollisions && e.length > 0 && ($("#fail-message").innerHTML = "You made contact with the International Space Station.", hideInterface("fail")), GMisCollisions && Math.abs(camera.position.distanceTo(GMGravBodyCenter)) < GMGravBodyAtmosphereRadius && ($("#fail-message").innerHTML = "You re-entered earth atmosphere.", hideInterface("fail")), GMisSpaceRestricted && t > 500 && ($("#fail-message").innerHTML = "You are too far away from the International Space Station.", hideInterface("fail")), GMisFuelLimit && GMDeltaVUsed > GMDeltaVBudget && ($("#fail-message").innerHTML = "You used too much propellant.", hideInterface("fail")), t < .2 && t2 < t) {
var GMInLimits = true, l = ""
if (GMisLimitsOriginal) {
if (!(a <= toleranceRotation && r <= toleranceRotation && o <= toleranceRotation)) {
l=GMAddReason(l,"ROTATION ANGLE")
GMInLimits = false
}
if (!(i <= toleranceRate && n <= toleranceRate && s <= toleranceRate)) {
l=GMAddReason(l,"SPEED")
GMInLimits = false
}
} else {
// correct speeds for m/s instead of m/frame
i=i/GMFixedStepTime
n=n/GMFixedStepTime
s=s/GMFixedStepTime
rxy=rxy/GMFixedStepTime
rz=rz/GMFixedStepTime
var dx = Math.abs(camera.position.x-issObject.position.x),
dy = Math.abs(camera.position.y-issObject.position.y),
dz = Math.abs(camera.position.z-issObject.position.z)
if (dz>=0.1) {
l=GMAddReason(l,"OFFSET-X > 0.1")
GMInLimits = false
}
if (dx>=0.1) {
l=GMAddReason(l,"OFFSET-Y > 0.1")
GMInLimits = false
}
if (dy>=0.1) {
l=GMAddReason(l,"OFFSET-Z > 0.1")
GMInLimits = false
}
if (s>=0.1) {
l=GMAddReason(l,"VELOCITY-X > 0.1")
GMInLimits = false
}
if (i>=0.1) {
l=GMAddReason(l,"VELOCITY-Y > 0.1")
GMInLimits = false
}
if (n>=0.1) {
l=GMAddReason(l,"VELOCITY-Z > 0.1")
GMInLimits = false
}
if (ar>=4) {
l=GMAddReason(l,"ANGLE (PITCH/YAW) > 4")
GMInLimits = false
}
if (o>=4) {
l=GMAddReason(l,"ROLL ANGLE > 4")
GMInLimits = false
}
if (rxy>=0.2) {
l=GMAddReason(l,"ROTATION (PITCH/YAW) > 0.2")
GMInLimits = false
}
if (rz>=0.2) {
l=GMAddReason(l,"ROLL RATE > 0.2")
GMInLimits = false
}
}
if (GMInLimits) {
hideInterface("success")
} else {
$("#fail-message").innerHTML = "The following errors occurred: <span class='red'>" + l + "<span>"
hideInterface("fail")
}
}
}
}
renderTesla = function() {
(!GMisTeslaHidden || camera.rotation.y > 2.5 || camera.rotation.y < -2.5 ? (isTeslaCreated || createTesla(), isTeslaLoaded && (teslaMesh.visible = !0)) : isTeslaLoaded && (teslaMesh.visible = !1))
}
// The NAVBall became a bit more complicated, since we would like it to be in Earth frame
// but we are in a rotating reference frame in an inclined earth orbit
// Helper function to calculate rotation between 2 orthogonal vector pairs
function GMRotateVectorPairs(u0, v0, u2, v2) {
var q2 = new THREE.Quaternion().setFromUnitVectors(u0, u2)
var v1 = v2.clone().applyQuaternion(q2.clone().conjugate())
var v0_proj = v0.projectOnPlane(u0)
var v1_proj = v1.projectOnPlane(u0)
var angleInPlane = v0_proj.angleTo(v1_proj)
if (v1_proj.dot(new THREE.Vector3().crossVectors(u0, v0)) < 0) {
angleInPlane *= -1
}
var q1 = new THREE.Quaternion().setFromAxisAngle(u0, angleInPlane)
var q = new THREE.Quaternion().multiplyQuaternions(q2, q1)
return q
}
// override navball rendering to show earth/orbit instead of ISS relative
renderNavball = function() {
if (navballCreated) {
var rotAxis=new THREE.Vector3(0,1,0)
var pAxis=new THREE.Vector3(0,0,1)
var camVector = camera.position.clone().sub(GMGravBodyCenter).normalize()
var earthRotAxis = rotAxis.clone().applyQuaternion(earthObject.quaternion).normalize()
var vectorToEast = camVector.clone().cross(earthRotAxis).normalize()
var q = GMRotateVectorPairs(rotAxis,pAxis,camVector,vectorToEast)
var e = camera.quaternion.clone().conjugate().multiply(q)
navballObject.setRotationFromQuaternion(e)
}
}
renderOrb = function() {
if (isOrbCreated) {
orbYawObject.rotation.y = GMisHUDVisible ? -camera.rotation.y : 0.0, orbPitchObject.rotation.x = GMisHUDVisible ? -camera.rotation.x : 0.0, orbRollObject.rotation.z = GMisHUDVisible ? -camera.rotation.z : 0.0
var e = orbFadeTarget.getWorldPosition(new THREE.Vector3), i
for (i = 0; i < orbSpriteArray.length; i++) {
var t = orbSpriteArray[i].getWorldPosition(new THREE.Vector3),
a = Math.abs(t.distanceTo(e))
orbSpriteArray[i].material.uniforms.u_opacity.value = GMisHUDVisible ? opacityDistanceClamp(a, 5, 2, 1) : 0.0
}
}
}
// this is just a bugfix for rough versus fine rotation control
updatePrecision = function(e) {
"rotation" === e && (rotationPulseSize = .5 === rotationPulseSize ? (rateSpeedSize=2,1) : (rateSpeedSize=1,.5), $("#rotation-controls").classList.toggle("large"), $("#precision-rotation-status").classList.toggle("large"), $("#hud-tips").classList.toggle("rotation-large")), "translation" === e && (translationPulseSize = .001 === translationPulseSize ? .005 : .001, $("#translation-controls").classList.toggle("large"), $("#precision-translation-status").classList.toggle("large"), $("#hud-tips").classList.toggle("translation-large"))
}
// this is overridden to reset some variables each new game/attempt
GMGameStart = function() {
GMDeltaVUsed=0
GMOldMotionVector.copy(motionVector)
GMOldRotation.set(currentRotationX, currentRotationY, currentRotationZ)
GMMissionTimer=0
GMBurnTimer=0
GMBurn=0
GMTimeLapse=1
GMGameTimer=0
isGameOver=!1
}
$("#fail-button").removeEventListener("click", showInterface, !1)
$("#fail-button").removeEventListener("touchstart", showInterface, !1)
$("#success-button").removeEventListener("click", showInterface, !1)
$("#success-button").removeEventListener("touchstart", showInterface, !1)
// the annoying thing is the need to duplicate the entire function
// for the change of a single onComplete property
window.GMshowInterface = function() {
showInterface()
setTimeout(GMGameStart,5000)
}
$("#fail-button").addEventListener("click", GMshowInterface, !1)
$("#fail-button").addEventListener("touchstart", GMshowInterface, !1)
$("#success-button").addEventListener("click", GMshowInterface, !1)
$("#success-button").addEventListener("touchstart", GMshowInterface, !1)
// extra keys - z/y for toggle translation precision - x for toggle rotation precision, c,v,b for time lapse
document.addEventListener("keyup", function(e) {
if (!isGameOver) switch (e.keyCode || e.which) {
case 89:
case 90:
toggleTranslation()
break;
case 88:
toggleRotation()
break;
case 67:
GMTimeLapse=1
break;
case 77: //M key - main burn & timer
GMBurn == 1 ? GMBurn=0 : GMBurn=1
if (GMBurn == 0) GMBurnTimer=0
break;
case 86:
GMTimeLapse>=10 ? GMTimeLapse/=10 : GMTimeLapse=1
break;
case 66:
GMTimeLapse<=100 ? GMTimeLapse*=10 : GMTimeLapse=1000
break;
}
})
// make 30 spheres for trajectory visualization
window.GMTrajectoryObject = new THREE.Object3D()
var GMTrajectoryLength = 30
for (var t=0; t<GMTrajectoryLength; t++) {
var scale=0.25
if (((t+1)%10)==0) {
scale=0.75
}
GMTrajectoryObject.add(new THREE.Mesh(new THREE.SphereGeometry(scale,6,6), new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0.75,
color: 2413309,
})))
}
GMTrajectoryObject.visible=false
scene.add(GMTrajectoryObject)
$("#intro-step1 .animate").innerHTML="<span class=\"red\">ISS-SIM-Customizer</span> user script v 0.000016 by <span class=\"green\">CorvusCorax</span> loaded succesfully - check settings page for details.<br/>Use [x] and [y]/[z] keys to toggle precision thrust for translation/rotation.<br/>Use [b],[v] for time warp +-, [c] resets time warp.<br/>"+$("#intro-step1 .animate").innerHTML
}
// delayed execution after SpaceX sim loads
var GMscript = document.createElement('script')
GMscript.type = "text/javascript"
GMscript.innerHTML = GMInitializer.toString() + "\n" + "window.addEventListener('load', GMInitializer)" + "\n"
document.getElementsByTagName('head')[0].appendChild(GMscript)
//end
@CorvusCorax
Copy link
Author

You could fork the gist on your own github, then I can pull the fragments :) It's basically a repository. where each comment or edit is a commit. or we turn it into a proper source code repository for development, what do you think?

@LeighDarby
Copy link

I created a fork and added the code fragments :-
https://gist.github.com/LeighDarby/2bb251105119ff27cdff80a9312cf96a

Was considering a couple of (possibly mad) ideas ...

Add an external camera view of Dragon (track relative and/or fixed point to view docking etc.) - there are quite a few 3D (.glb) models available but the ones with docking nose cone open all require $$ I think ...

Found a decent .glb of Hubble, could put that into orbit and have a different rendezvous option?
Wikipedia seems to have some orbital data but perhaps NASA has better not had a chance to look yet ...

@CorvusCorax
Copy link
Author

CorvusCorax commented May 18, 2021 via email

@CorvusCorax
Copy link
Author

CorvusCorax commented Sep 30, 2021

I created a fork and added the code fragments :- https://gist.github.com/LeighDarby/2bb251105119ff27cdff80a9312cf96a

I pulled your changes and integrated this in the main code. Since Dragon's main thrusters are pointed forward - located under the nose cone - I changed the main thrusters to firing backwards instead of forward. I also added code to reset burn timer and thruster status whenever the simulation is reset to not "spawn" with firing main engine ;)

After playing a bit, I think that the thrust is probably way too much. Dragon has 4 forward draco thrusters with 400 N each. Assuming a mass of aprox 10 tons, that would lead to an acceleration of 1600N / 10000kg = 0.16 m/s² when firing continuously.
When hammering the controls, a user can easily achieve 10 times as much and your "main engine" script in course mode achieves close to 1 g - 100 times the thrust dragon would actually have.

but it is of course quite convenient ;) I think I leave it in ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment