code: function recomputeDirections(queryModel, relativeLightPosition) {
    // construct a coordinate system relative to eye point
    const v = spherical2Cartesian(
      queryModel.getValue('phi'),
      queryModel.getValue('theta')
    );
    const viewDir = vec3.fromValues(v[0], v[1], v[2]);
    const at = vec3.fromValues(0, 0, 0); // assumption always looking at 0
    const north = vec3.fromValues(0, 0, 1); // assumption, north is always up
    const approxUp = vec3.create();
    vec3.add(approxUp, north, viewDir);
    vec3.normalize(approxUp, approxUp);
  
    const t0 = vec3.create();
    vec3.subtract(t0, at, viewDir);
    const t1 = vec3.create();
    vec3.subtract(t1, approxUp, viewDir);
    const right = vec3.create();
    vec3.cross(right, t0, t1);
    vec3.normalize(right, right);
    vec3.subtract(t0, right, viewDir);
    vec3.subtract(t1, at, viewDir);
    const up = vec3.create();
    vec3.cross(up, t0, t1);
    vec3.normalize(up, up);
    // scale down so we can alway have room before normalization
    const rm = vec3.create();
    vec3.scale(rm, right, relativeLightPosition.x);
    const um = vec3.create();
    vec3.scale(um, up, relativeLightPosition.y);
    const scaledView = vec3.create();
    vec3.scale(scaledView, viewDir, 0.3);
    const lightDirection = vec3.create();
    vec3.add(lightDirection, scaledView, rm);
    vec3.add(lightDirection, lightDirection, um);
    vec3.normalize(lightDirection, lightDirection);
    return {
      lightDir: lightDirection,
      viewDir,
    };
  }
docs: --------------------------------------------------------------------------