# WebVR Stereo Camera Not Working

**URL:** <https://discourse.threejs.org/t/webvr-stereo-camera-not-working/4369>\
**Category:** Questions\
**Tags:** webvr\
**Created:** [October 5, 2018, 2:40am UTC](https://discourse.threejs.org/t/webvr-stereo-camera-not-working/4369 "2018-10-05T02:40:17Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Shawn\_Rosofsky](https://avatars.discourse-cdn.com/v4/letter/s/73ab20/32.png) [@Shawn\_Rosofsky](https://discourse.threejs.org/u/Shawn_Rosofsky)\
**Post date:** [October 5, 2018, 2:40am UTC](https://discourse.threejs.org/t/webvr-stereo-camera-not-working/4369/1 "2018-10-05T02:40:18Z")

</div>

I have been trying to convert the git rep I found [here](https://github.com/oseiskar/black-hole) to be VR compatible. The issue that I am having is that when I render a the image in VR mode I see the VR glasses filter overlaid on top of what appears to be a single image, rather than 2 separate images placed very close to each other in stereo camera like seen bellow.

![image](https://cdck-file-uploads-canada1.s3.dualstack.ca-central-1.amazonaws.com/flex035/uploads/threejs/original/2X/0/06e09551139f2e6bf38702c5023e2af0a447b775.jpeg)

After some playing around with the settings, I think the issue may be in how the texture is being rendered and applied to the geometry, but that is just a guess.

Any help in getting the stereo to work properly would be very much appreciated. See my code bellow.

This post is the same issue as my unanswered question on stack-exchange [here](https://stackoverflow.com/questions/52632929/vr-inverted-tracking-and-incorrect-camera-seperation-using-three-js-and-webvr).

> **main.js:**
>
> ```
> "use strict"
> /*global THREE, SHADER_LOADER, Mustache, Stats, Detector, $, dat:false */
> /*global document, window, setTimeout, requestAnimationFrame:false */
> /*global ProceduralTextures:false */
> 
> if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
> 
> function Observer() {
> this.position = new THREE.Vector3(10,0,0);
> this.velocity = new THREE.Vector3(0,1,0);
> this.orientation = new THREE.Matrix3();
> this.time = 0.0;
> }
> 
> Observer.prototype.orbitalFrame = function() {
> 
> //var orbital_y = observer.velocity.clone().normalize();
> var orbital_y = (new THREE.Vector3())
> .subVectors(observer.velocity.clone().normalize().multiplyScalar(4.0),
> observer.position).normalize();
> 
> var orbital_z = (new THREE.Vector3())
> .crossVectors(observer.position, orbital_y).normalize();
> var orbital_x = (new THREE.Vector3()).crossVectors(orbital_y, orbital_z);
> 
> return (new THREE.Matrix4()).makeBasis(
> orbital_x,
> orbital_y,
> orbital_z
> ).linearPart();
> };
> 
> Observer.prototype.move = function(dt) {
> 
> dt *= shader.parameters.time_scale;
> 
> var r;
> var v = 0;
> 
> // motion on a pre-defined cirular orbit
> if (shader.parameters.observer.motion) {
> 
> r = shader.parameters.observer.distance;
> v = 1.0 / Math.sqrt(2.0*(r-1.0));
> var ang_vel = v / r;
> var angle = this.time * ang_vel;
> 
> var s = Math.sin(angle), c = Math.cos(angle);
> 
> this.position.set(c*r, s*r, 0);
> this.velocity.set(-s*v, c*v, 0);
> 
> var alpha = degToRad(shader.parameters.observer.orbital_inclination);
> var orbit_coords = (new THREE.Matrix4()).makeRotationY(alpha);
> 
> this.position.applyMatrix4(orbit_coords);
> this.velocity.applyMatrix4(orbit_coords);
> }
> else {
> r = this.position.length();
> }
> 
> if (shader.parameters.gravitational_time_dilation) {
> dt = Math.sqrt((dt*dt * (1.0 - v*v)) / (1-1.0/r));
> }
> 
> this.time += dt;
> };
> 
> var container, stats;
> var camera, scene, renderer, cameraControls, shader=null, dollycam;
> var manager;
> var observer = new Observer();
> var effect;
> var vrDisplay;
> 
> function Shader(mustacheTemplate) {
> // Compile-time shader parameters
> this.parameters = {
> n_steps: 100,
> quality: 'medium',
> accretion_disk: true,
> planet: {
> enabled: true,
> distance: 4.0,
> radius: 0.5
> },
> lorentz_contraction: true,
> gravitational_time_dilation: true,
> aberration: true,
> beaming: true,
> doppler_shift: true,
> light_travel_time: true,
> time_scale: 1.0,
> observer: {
> motion: true,
> distance: 7.0,
> orbital_inclination: -10
> },
> 
> planetEnabled: function() {
> // return this.planet.enabled && this.quality !== 'fast';
> return this.planet.enabled;
> },
> 
> observerMotion: function() {
> return this.observer.motion;
> }
> };
> var that = this;
> this.needsUpdate = false;
> 
> this.hasMovingParts = function() {
> return this.parameters.planet.enabled || this.parameters.observer.motion;
> };
> 
> this.compile = function() {
> return Mustache.render(mustacheTemplate, that.parameters);
> };
> }
> 
> function degToRad(a) { return Math.PI * a / 180.0; }
> 
> (function(){
> var textures = {};
> 
> function whenLoaded() {
> init(textures);
> $('#loader').hide();
> $('.initially-hidden').removeClass('initially-hidden');
> animate();
> }
> 
> function checkLoaded() {
> if (shader === null) return;
> for (var key in textures) if (textures[key] === null) return;
> whenLoaded();
> }
> 
> SHADER_LOADER.load(function(shaders) {
> shader = new Shader(shaders.raytracer.fragment);
> checkLoaded();
> });
> 
> var texLoader = new THREE.TextureLoader();
> function loadTexture(symbol, filename, interpolation) {
> textures[symbol] = null;
> texLoader.load(filename, function(tex) {
> tex.magFilter = interpolation;
> tex.minFilter = interpolation;
> textures[symbol] = tex;
> checkLoaded();
> });
> }
> 
> loadTexture('galaxy', 'img/milkyway.jpg', THREE.NearestFilter);
> loadTexture('spectra', 'img/spectra.png', THREE.LinearFilter);
> loadTexture('moon', 'img/beach-ball.png', THREE.LinearFilter);
> loadTexture('stars', 'img/stars.png', THREE.LinearFilter);
> loadTexture('accretion_disk', 'img/accretion-disk.png', THREE.LinearFilter);
> })();
> 
> var updateUniforms;
> 
> function init(textures) {
> 
> container = document.createElement( 'div' );
> document.body.appendChild( container );
> 
> scene = new THREE.Scene();
> 
> var geometry = new THREE.PlaneBufferGeometry( 2, 2 ,30,30);
> // var geometry = new THREE.SphereBufferGeometry( 100, 100, 100 );
> // geometry.scale( 1, 1, - 1 );
> var uniforms = {
> time: { type: "f", value: 0 },
> resolution: { type: "v2", value: new THREE.Vector2() },
> cam_pos: { type: "v3", value: new THREE.Vector3() },
> cam_x: { type: "v3", value: new THREE.Vector3() },
> cam_y: { type: "v3", value: new THREE.Vector3() },
> cam_z: { type: "v3", value: new THREE.Vector3() },
> cam_vel: { type: "v3", value: new THREE.Vector3() },
> 
> planet_distance: { type: "f" },
> planet_radius: { type: "f" },
> 
> star_texture: { type: "t", value: textures.stars },
> accretion_disk_texture: { type: "t", value: textures.accretion_disk },
> galaxy_texture: { type: "t", value: textures.galaxy },
> planet_texture: { type: "t", value: textures.moon },
> spectrum_texture: { type: "t", value: textures.spectra }
> };
> 
> updateUniforms = function() {
> uniforms.planet_distance.value = shader.parameters.planet.distance;
> uniforms.planet_radius.value = shader.parameters.planet.radius;
> 
> uniforms.resolution.value.x = renderer.domElement.width;
> uniforms.resolution.value.y = renderer.domElement.height;
> 
> uniforms.time.value = observer.time;
> uniforms.cam_pos.value = observer.position;
> 
> var e = observer.orientation.elements;
> 
> uniforms.cam_x.value.set(e[0], e[1], e[2]);
> uniforms.cam_y.value.set(e[3], e[4], e[5]);
> uniforms.cam_z.value.set(e[6], e[7], e[8]);
> 
> function setVec(target, value) {
> uniforms[target].value.set(value.x, value.y, value.z);
> }
> 
> setVec('cam_pos', observer.position);
> setVec('cam_vel', observer.velocity);
> };
> 
> var material = new THREE.ShaderMaterial( {
> uniforms: uniforms,
> vertexShader: $('#vertex-shader').text(),
> });
> 
> scene.updateShader = function() {
> material.fragmentShader = shader.compile();
> material.needsUpdate = true;
> shader.needsUpdate = true;
> };
> 
> scene.updateShader();
> 
> var mesh = new THREE.Mesh( geometry, material );
> scene.add( mesh );
> 
> renderer = new THREE.WebGLRenderer();
> // manager = new WebVRManager(renderer);
> renderer.setPixelRatio( window.devicePixelRatio );
> renderer.setSize( window.innerWidth, window.innerHeight );
> renderer.vr.enabled = true; // added vr
> // window.addEventListener('vrdisplayactivate', function () {
> // device.requestPresent([{source: renderer.domElement}]);
> // });   
> // renderer.vr.setDevice(renderer.domElement);
> // renderer.vr.setDevice(vrDisplay);
> // vrDisplay.requestPresent([
> // {
> // source: renderer.domElement
> // }
> // ]);
> document.body.appendChild( renderer.domElement );
> // effect = new THREE.StereoEffect( renderer );
> // effect.setSize( window.innerWidth, window.innerHeight );
> // navigator.getVRDisplays().then(displays => renderer.vr.setDevice(displays[0]));
> // document.body.appendChild( WEBVR.createButton( renderer ) );
> // WEBVR.getVRDisplay( function ( device ) {
> 
> // renderer.vr.setDevice( device )
> // document.body.appendChild( WEBVR.getButton( device, renderer.domElement ) );
>     
> // } );
>     
> document.body.appendChild( WEBVR.createButton( renderer) );
> // WebVRManager(renderer);
> // renderer.autoClear = false;  
> 
>     
> stats = new Stats();
> stats.domElement.style.position = 'absolute';
> stats.domElement.style.top = '0px';
> container.appendChild( stats.domElement );
> $(stats.domElement).addClass('hidden-phone');
> 
> // Orbit camera from three.js
> camera = new THREE.PerspectiveCamera(300, window.innerWidth / window.innerHeight, .1, 10000 );
> // camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
> // initializeCamera(camera);
> //initiallize camera
> var pitchAngle = 0.0, yawAngle =0.0;
> 
> // there are nicely named methods such as "lookAt" in the camera object
> // but there do not do a thing to the projection matrix due to an internal
> // representation of the camera coordinates using a quaternion (nice)
> camera.matrixWorldInverse.makeRotationX(degToRad(-pitchAngle));
> camera.matrixWorldInverse.multiply(new THREE.Matrix4().makeRotationY(degToRad(-yawAngle)));
> 
> var m = camera.matrixWorldInverse.elements;
> dollycam = new THREE.PerspectiveCamera();
> dollycam.position.set(m[2], m[6], m[10]);
> camera.position.set(m[2],m[6],m[10]);
> // camera.aspect = window.innerWidth / window.innerHeight;
>     
> 
> dollycam.add(camera);
> scene.add(dollycam);
> 
> cameraControls = new THREE.OrbitControls( dollycam, renderer.domElement );
> 
> cameraControls.target.set( 0, 0, 0 );
> cameraControls.addEventListener( 'change', updateCamera );
> // cameraControls.update()
> updateCamera();
> 
> window.addEventListener( 'resize', onWindowResize, false );
> 
> onWindowResize();
> 
> setupGUI();
> }
> 
> function setupGUI() {
> 
> var hint = $('#hint-text');
> var p = shader.parameters;
> 
> function updateShader() {
> hint.hide();
> scene.updateShader();
> }
> 
> var gui = new dat.GUI();
> gui.close();
> 
> gui.add(p, 'quality', ['fast', 'medium', 'high']).onChange(function (value) {
> $('.planet-controls').show();
> switch(value) {
> case 'fast':
> p.n_steps = 40;
> // $('.planet-controls').hide();
> break;
> case 'medium':
> p.n_steps = 100;
> break;
> case 'high':
> p.n_steps = 200;
> break;
> }
> 
> updateShader();
> });
> gui.add(p, 'accretion_disk').onChange(updateShader);
> 
> var folder = gui.addFolder('Observer');
> folder.add(p.observer, 'motion').onChange(function(motion) {
> updateCamera();
> updateShader();
> // if (motion) {
> // hint.text('Moving observer; drag to rotate camera');
> // } else {
> // hint.text('Stationary observer; drag to orbit around');
> // }
> // hint.fadeIn();
> });
> folder.add(p.observer, 'distance').min(1.5).max(30).onChange(updateCamera);
> folder.add(p.observer, 'orbital_inclination').min(-90).max(90).onChange(updateCamera);
> folder.open();
> 
> folder = gui.addFolder('Planet');
> folder.add(p.planet, 'enabled').onChange(function(enabled) {
> updateShader();
> var controls = $('.indirect-planet-controls').show();
> if (enabled) controls.show();
> else controls.hide();
> });
> folder.add(p.planet, 'distance').min(1.5).onChange(updateUniforms);
> folder.add(p.planet, 'radius').min(0.01).max(2.0).onChange(updateUniforms);
> $(folder.domElement).addClass('planet-controls');
> //folder.open();
> 
> function setGuiRowClass(guiEl, klass) {
> $(guiEl.domElement).parent().parent().addClass(klass);
> }
> 
> folder = gui.addFolder('Relativistic effects');
> folder.add(p, 'aberration').onChange(updateShader);
> folder.add(p, 'beaming').onChange(updateShader);
> folder.add(p, 'doppler_shift').onChange(updateShader);
> setGuiRowClass(
> folder.add(p, 'gravitational_time_dilation').onChange(updateShader),
> 'planet-controls indirect-planet-controls');
> setGuiRowClass(
> folder.add(p, 'lorentz_contraction').onChange(updateShader),
> 'planet-controls indirect-planet-controls');
> 
> // folder.open();
> 
> folder = gui.addFolder('Time');
> folder.add(p, 'light_travel_time').onChange(updateShader);
> folder.add(p, 'time_scale').min(0);
> //folder.open();
> gui.close();
> 
> }
> 
> function onWindowResize( event ) {
> camera.aspect = window.innerWidth / window.innerHeight;
> camera.updateProjectionMatrix();
> renderer.setSize( window.innerWidth, window.innerHeight );
> // effect.setSize( window.innerWidth, window.innerHeight );
> updateUniforms();
> }
> 
> function updateCamera( event ) {
> 
> var zoom_dist = camera.position.length();
> // var m = camera.matrixWorldInverse.elements;
> // camera.updateMatrixWorld();
> var m = camera.matrixWorld.elements;
> var camera_matrix;
> 
> if (shader.parameters.observer.motion) {
> camera_matrix = new THREE.Matrix3();
> }
> else{
> camera_matrix = observer.orientation;
> }
> 
> camera_matrix.set(
> // row-major, not the same as .elements (nice)
> // y and z swapped for a nicer coordinate system
> m[0], m[1], m[2],
> m[8], m[9], m[10],
> m[4], m[5], m[6]
> );
> 
> observer.orientation = observer.orbitalFrame().multiply(camera_matrix);
> 
> }
> 
> function frobeniusDistance(matrix1, matrix2) {
> var sum = 0.0;
> for (var i in matrix1.elements) {
> var diff = matrix1.elements[i] - matrix2.elements[i];
> sum += diff*diff;
> }
> return Math.sqrt(sum);
> }
> 
> function animate() {
> // requestAnimationFrame( animate );
>     
> // renderer.setAnimationLoop( renderer.render( scene, camera ) );
> renderer.setAnimationLoop(render);
> 
> }
> 
> var lastCameraMat = new THREE.Matrix4().identity();
> 
> var getFrameDuration = (function() {
> var lastTimestamp = new Date().getTime();
> return function() {
> var timestamp = new Date().getTime();
> var diff = (timestamp - lastTimestamp) / 1000.0;
> lastTimestamp = timestamp;
> return diff;
> };
> })();
> 
> function render() {
> // renderer.render( scene, camera );
> camera.updateMatrixWorld();
> camera.matrixWorldInverse.getInverse( camera.matrixWorld );
> // dollycam.updateMatrixWorld();
> // dollycam.matrixWorldInverse.getInverse( dollycam.matrixWorld );
> // cameraControls.update();
>     
> observer.move(getFrameDuration());
> // cameraControls.update();
> updateCamera();
> updateUniforms();
> 
> // render();
> renderer.render( scene, camera );
> 
> stats.update();
> 
> }
> 
> ```

> **raytracer.glsl**
>
> ```
> #define M_PI 3.141592653589793238462643383279
> #define R_SQRT_2 0.7071067811865475
> #define DEG_TO_RAD (M_PI/180.0)
> #define SQ(x) ((x)*(x))
> 
> #define ROT_Y(a) mat3(0, cos(a), sin(a), 1, 0, 0, 0, sin(a), -cos(a))
> 
> // spectrum texture lookup helper macros
> const float BLACK_BODY_TEXTURE_COORD = 1.0;
> const float SINGLE_WAVELENGTH_TEXTURE_COORD = 0.5;
> const float TEMPERATURE_LOOKUP_RATIO_TEXTURE_COORD = 0.0;
> 
> // black-body texture metadata
> const float SPECTRUM_TEX_TEMPERATURE_RANGE = 65504.0;
> const float SPECTRUM_TEX_WAVELENGTH_RANGE = 2048.0;
> const float SPECTRUM_TEX_RATIO_RANGE = 6.48053329012;
> 
> // multi-line macros don't seem to work in WebGL :(
> #define BLACK_BODY_COLOR(t) texture2D(spectrum_texture, vec2((t) / SPECTRUM_TEX_TEMPERATURE_RANGE, BLACK_BODY_TEXTURE_COORD))
> #define SINGLE_WAVELENGTH_COLOR(lambda) texture2D(spectrum_texture, vec2((lambda) / SPECTRUM_TEX_WAVELENGTH_RANGE, SINGLE_WAVELENGTH_TEXTURE_COORD))
> #define TEMPERATURE_LOOKUP(ratio) (texture2D(spectrum_texture, vec2((ratio) / SPECTRUM_TEX_RATIO_RANGE, TEMPERATURE_LOOKUP_RATIO_TEXTURE_COORD)).r * SPECTRUM_TEX_TEMPERATURE_RANGE)
> 
> uniform vec2 resolution;
> uniform float time;
> 
> uniform vec3 cam_pos;
> uniform vec3 cam_x;
> uniform vec3 cam_y;
> uniform vec3 cam_z;
> uniform vec3 cam_vel;
> 
> uniform float planet_distance, planet_radius;
> 
> uniform sampler2D galaxy_texture, star_texture,
> accretion_disk_texture, planet_texture, spectrum_texture;
> 
> // stepping parameters
> const int NSTEPS = {{n_steps}};
> const float MAX_REVOLUTIONS = 2.0;
> 
> const float ACCRETION_MIN_R = 1.5;
> const float ACCRETION_WIDTH = 5.0;
> const float ACCRETION_BRIGHTNESS = 0.9;
> const float ACCRETION_TEMPERATURE = 3900.0;
> 
> const float STAR_MIN_TEMPERATURE = 4000.0;
> const float STAR_MAX_TEMPERATURE = 15000.0;
> 
> const float STAR_BRIGHTNESS = 1.0;
> const float GALAXY_BRIGHTNESS = 0.4;
> 
> const float PLANET_AMBIENT = 0.1;
> const float PLANET_LIGHTNESS = 1.5;
> 
> // background texture coordinate system
> mat3 BG_COORDS = ROT_Y(45.0 * DEG_TO_RAD);
> 
> // planet texture coordinate system
> const float PLANET_AXIAL_TILT = 30.0 * DEG_TO_RAD;
> mat3 PLANET_COORDS = ROT_Y(PLANET_AXIAL_TILT);
> 
> const float FOV_ANGLE_DEG = 90.0;
> float FOV_MULT = 1.0 / tan(DEG_TO_RAD * FOV_ANGLE_DEG*0.5);
> 
> // derived "constants" (from uniforms)
> float PLANET_RADIUS,
> PLANET_DISTANCE,
> PLANET_ORBITAL_ANG_VEL,
> PLANET_ROTATION_ANG_VEL,
> PLANET_GAMMA;
> 
> vec2 sphere_map(vec3 p) {
> return vec2(atan(p.x,p.y)/M_PI*0.5+0.5, asin(p.z)/M_PI+0.5);
> }
> 
> float smooth_step(float x, float threshold) {
> const float STEEPNESS = 1.0;
> return 1.0 / (1.0 + exp(-(x-threshold)*STEEPNESS));
> }
> 
> vec3 lorentz_velocity_transformation(vec3 moving_v, vec3 frame_v) {
> float v = length(frame_v);
> if (v > 0.0) {
> vec3 v_axis = -frame_v / v;
> float gamma = 1.0/sqrt(1.0 - v*v);
> 
> float moving_par = dot(moving_v, v_axis);
> vec3 moving_perp = moving_v - v_axis*moving_par;
> 
> float denom = 1.0 + v*moving_par;
> return (v_axis*(moving_par+v)+moving_perp/gamma)/denom;
> }
> return moving_v;
> }
> 
> vec3 contract(vec3 x, vec3 d, float mult) {
> float par = dot(x,d);
> return (x-par*d) + d*par*mult;
> }
> 
> vec4 planet_intersection(vec3 old_pos, vec3 ray, float t, float dt,
> vec3 planet_pos0, float ray_doppler_factor) {
> 
> vec4 ret = vec4(0,0,0,0);
> vec3 ray0 = ray;
> ray = ray/dt;
> 
> vec3 planet_dir = vec3(planet_pos0.y, -planet_pos0.x, 0.0) / PLANET_DISTANCE;
> 
> {{#light_travel_time}}
> float planet_ang1 = (t-dt) * PLANET_ORBITAL_ANG_VEL;
> vec3 planet_pos1 = vec3(cos(planet_ang1), sin(planet_ang1), 0)*PLANET_DISTANCE;
> vec3 planet_vel = (planet_pos1-planet_pos0)/dt;
> 
> // transform to moving planet coordinate system
> ray = ray - planet_vel;
> {{/light_travel_time}}
> {{^light_travel_time}}
> vec3 planet_vel = planet_dir * PLANET_ORBITAL_ANG_VEL * PLANET_DISTANCE;
> {{/light_travel_time}}
> 
> // ray-sphere intersection
> vec3 d = old_pos - planet_pos0;
> 
> {{#lorentz_contraction}}
> ray = contract(ray, planet_dir, PLANET_GAMMA);
> d = contract(d, planet_dir, PLANET_GAMMA);
> {{/lorentz_contraction}}
> 
> float dotp = dot(d,ray);
> float c_coeff = dot(d,d) - SQ(PLANET_RADIUS);
> float ray2 = dot(ray, ray);
> float discr = dotp*dotp - ray2*c_coeff;
> 
> if (discr < 0.0) return ret;
> float isec_t = (-dotp - sqrt(discr)) / ray2;
> 
> float MIN_ISEC_DT = 0.0;
> {{#lorentz_contraction}}
> MIN_ISEC_DT = -dt;
> {{/lorentz_contraction}}
> 
> if (isec_t < MIN_ISEC_DT || isec_t > dt) return ret;
> 
> vec3 surface_point = (d + isec_t*ray) / PLANET_RADIUS;
> 
> isec_t = isec_t/dt;
> 
> vec3 light_dir = planet_pos0;
> float rot_phase = t;
> 
> {{#light_travel_time}}
> light_dir += planet_vel*isec_t*dt;
> rot_phase -= isec_t*dt;
> {{/light_travel_time}}
> 
> rot_phase = rot_phase * PLANET_ROTATION_ANG_VEL*0.5/M_PI;
> light_dir = light_dir / PLANET_DISTANCE;
> 
> {{#light_travel_time}}
> light_dir = light_dir - planet_vel;
> {{/light_travel_time}}
> 
> vec3 surface_normal = surface_point;
> {{#lorentz_contraction}}
> light_dir = contract(light_dir, planet_dir, PLANET_GAMMA);
> {{/lorentz_contraction}}
> light_dir = normalize(light_dir);
> 
> vec2 tex_coord = sphere_map(surface_point * PLANET_COORDS);
> tex_coord.x = mod(tex_coord.x + rot_phase, 1.0);
> 
> float diffuse = max(0.0, dot(surface_normal, -light_dir));
> float lightness = ((1.0-PLANET_AMBIENT)*diffuse + PLANET_AMBIENT) *
> PLANET_LIGHTNESS;
> 
> float light_temperature = ACCRETION_TEMPERATURE;
> {{#doppler_shift}}
> float doppler_factor = SQ(PLANET_GAMMA) *
> (1.0 + dot(planet_vel, light_dir)) *
> (1.0 - dot(planet_vel, normalize(ray)));
> light_temperature /= doppler_factor * ray_doppler_factor;
> {{/doppler_shift}}
> 
> vec4 light_color = BLACK_BODY_COLOR(light_temperature);
> ret = texture2D(planet_texture, tex_coord) * lightness * light_color;
> if (isec_t < 0.0) isec_t = 0.5;
> ret.w = isec_t;
> 
> return ret;
> }
> 
> vec4 galaxy_color(vec2 tex_coord, float doppler_factor) {
> 
> vec4 color = texture2D(galaxy_texture, tex_coord);
> {{^observerMotion}}
> return color;
> {{/observerMotion}}
> 
> {{#observerMotion}}
> vec4 ret = vec4(0.0,0.0,0.0,0.0);
> float red = max(0.0, color.r - color.g);
> 
> const float H_ALPHA_RATIO = 0.1;
> const float TEMPERATURE_BIAS = 0.95;
> 
> color.r -= red*H_ALPHA_RATIO;
> 
> float i1 = max(color.r, max(color.g, color.b));
> float ratio = (color.g+color.b) / color.r;
> 
> if (i1 > 0.0 && color.r > 0.0) {
> 
> float temperature = TEMPERATURE_LOOKUP(ratio) * TEMPERATURE_BIAS;
> color = BLACK_BODY_COLOR(temperature);
> 
> float i0 = max(color.r, max(color.g, color.b));
> if (i0 > 0.0) {
> temperature /= doppler_factor;
> ret = BLACK_BODY_COLOR(temperature) * max(i1/i0,0.0);
> }
> }
> 
> ret += SINGLE_WAVELENGTH_COLOR(656.28 * doppler_factor) * red / 0.214 * H_ALPHA_RATIO;
> 
> return ret;
> {{/observerMotion}}
> }
> 
> void main() {
> 
> {{#planetEnabled}}
> // "constants" derived from uniforms
> PLANET_RADIUS = planet_radius;
> PLANET_DISTANCE = max(planet_distance,planet_radius+1.5);
> PLANET_ORBITAL_ANG_VEL = -1.0 / sqrt(2.0*(PLANET_DISTANCE-1.0)) / PLANET_DISTANCE;
> float MAX_PLANET_ROT = max((1.0 + PLANET_ORBITAL_ANG_VEL*PLANET_DISTANCE) / PLANET_RADIUS,0.0);
> PLANET_ROTATION_ANG_VEL = -PLANET_ORBITAL_ANG_VEL + MAX_PLANET_ROT * 0.5;
> PLANET_GAMMA = 1.0/sqrt(1.0-SQ(PLANET_ORBITAL_ANG_VEL*PLANET_DISTANCE));
> {{/planetEnabled}}
> 
> vec2 p = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy;
> p.y *= resolution.y / resolution.x;
> 
> vec3 pos = cam_pos;
> vec3 ray = normalize(p.x*cam_x + p.y*cam_y + FOV_MULT*cam_z);
> 
> {{#aberration}}
> ray = lorentz_velocity_transformation(ray, cam_vel);
> {{/aberration}}
> 
> float ray_intensity = 1.0;
> float ray_doppler_factor = 1.0;
> 
> float gamma = 1.0/sqrt(1.0-dot(cam_vel,cam_vel));
> ray_doppler_factor = gamma*(1.0 + dot(ray,-cam_vel));
> {{#beaming}}
> ray_intensity /= ray_doppler_factor*ray_doppler_factor*ray_doppler_factor;
> {{/beaming}}
> {{^doppler_shift}}
> ray_doppler_factor = 1.0;
> {{/doppler_shift}}
> 
> float step = 0.01;
> vec4 color = vec4(0.0,0.0,0.0,1.0);
> 
> // initial conditions
> float u = 1.0 / length(pos), old_u;
> float u0 = u;
> 
> vec3 normal_vec = normalize(pos);
> vec3 tangent_vec = normalize(cross(cross(normal_vec, ray), normal_vec));
> 
> float du = -dot(ray,normal_vec) / dot(ray,tangent_vec) * u;
> float du0 = du;
> 
> float phi = 0.0;
> float t = time;
> float dt = 1.0;
> 
> {{^light_travel_time}}
> float planet_ang0 = t * PLANET_ORBITAL_ANG_VEL;
> vec3 planet_pos0 = vec3(cos(planet_ang0), sin(planet_ang0), 0)*PLANET_DISTANCE;
> {{/light_travel_time}}
> 
> vec3 old_pos;
> 
> for (int j=0; j < NSTEPS; j++) {
> 
> step = MAX_REVOLUTIONS * 2.0*M_PI / float(NSTEPS);
> 
> // adaptive step size, some ad hoc formulas
> float max_rel_u_change = (1.0-log(u))*10.0 / float(NSTEPS);
> if ((du > 0.0 || (du0 < 0.0 && u0/u < 5.0)) && abs(du) > abs(max_rel_u_change*u) / step)
> step = max_rel_u_change*u/abs(du);
> 
> old_u = u;
> 
> {{#light_travel_time}}
> {{#gravitational_time_dilation}}
> dt = sqrt(du*du + u*u*(1.0-u))/(u*u*(1.0-u))*step;
> {{/gravitational_time_dilation}}
> {{/light_travel_time}}
> 
> // Leapfrog scheme
> u += du*step;
> float ddu = -u*(1.0 - 1.5*u*u);
> du += ddu*step;
> 
> if (u < 0.0) break;
> 
> phi += step;
> 
> old_pos = pos;
> pos = (cos(phi)*normal_vec + sin(phi)*tangent_vec)/u;
> 
> ray = pos-old_pos;
> float solid_isec_t = 2.0;
> float ray_l = length(ray);
> 
> {{#light_travel_time}}
> {{#gravitational_time_dilation}}
> float mix = smooth_step(1.0/u, 8.0);
> dt = mix*ray_l + (1.0-mix)*dt;
> {{/gravitational_time_dilation}}
> {{^gravitational_time_dilation}}
> dt = ray_l;
> {{/gravitational_time_dilation}}
> {{/light_travel_time}}
> 
> {{#planetEnabled}}
> if (
> (
> old_pos.z * pos.z < 0.0 ||
> min(abs(old_pos.z), abs(pos.z)) < PLANET_RADIUS
> ) &&
> max(u, old_u) > 1.0/(PLANET_DISTANCE+PLANET_RADIUS) &&
> min(u, old_u) < 1.0/(PLANET_DISTANCE-PLANET_RADIUS)
> ) {
> 
> {{#light_travel_time}}
> float planet_ang0 = t * PLANET_ORBITAL_ANG_VEL;
> vec3 planet_pos0 = vec3(cos(planet_ang0), sin(planet_ang0), 0)*PLANET_DISTANCE;
> {{/light_travel_time}}
> 
> vec4 planet_isec = planet_intersection(old_pos, ray, t, dt,
> planet_pos0, ray_doppler_factor);
> if (planet_isec.w > 0.0) {
> solid_isec_t = planet_isec.w;
> planet_isec.w = 1.0;
> color += planet_isec;
> }
> }
> {{/planetEnabled}}
> 
> {{#accretion_disk}}
> if (old_pos.z * pos.z < 0.0) {
> // crossed plane z=0
> 
> float acc_isec_t = -old_pos.z / ray.z;
> if (acc_isec_t < solid_isec_t) {
> vec3 isec = old_pos + ray*acc_isec_t;
> 
> float r = length(isec);
> 
> if (r > ACCRETION_MIN_R) {
> vec2 tex_coord = vec2(
> (r-ACCRETION_MIN_R)/ACCRETION_WIDTH,
> atan(isec.x, isec.y)/M_PI*0.5+0.5
> );
> 
> float accretion_intensity = ACCRETION_BRIGHTNESS;
> //accretion_intensity *= 1.0 / abs(ray.z/ray_l);
> float temperature = ACCRETION_TEMPERATURE;
> 
> vec3 accretion_v = vec3(-isec.y, isec.x, 0.0) / sqrt(2.0*(r-1.0)) / (r*r);
> gamma = 1.0/sqrt(1.0-dot(accretion_v,accretion_v));
> float doppler_factor = gamma*(1.0+dot(ray/ray_l,accretion_v));
> {{#beaming}}
> accretion_intensity /= doppler_factor*doppler_factor*doppler_factor;
> {{/beaming}}
> {{#doppler_shift}}
> temperature /= ray_doppler_factor*doppler_factor;
> {{/doppler_shift}}
> 
> color += texture2D(accretion_disk_texture,tex_coord)
> * accretion_intensity
> * BLACK_BODY_COLOR(temperature);
> }
> }
> }
> {{/accretion_disk}}
> 
> {{#light_travel_time}}
> t -= dt;
> {{/light_travel_time}}
> 
> if (solid_isec_t <= 1.0) u = 2.0; // break
> if (u > 1.0) break;
> }
> 
> // the event horizon is at u = 1
> if (u < 1.0) {
> ray = normalize(pos - old_pos);
> vec2 tex_coord = sphere_map(ray * BG_COORDS);
> float t_coord;
> 
> vec4 star_color = texture2D(star_texture, tex_coord);
> if (star_color.r > 0.0) {
> t_coord = (STAR_MIN_TEMPERATURE +
> (STAR_MAX_TEMPERATURE-STAR_MIN_TEMPERATURE) * star_color.g)
> / ray_doppler_factor;
> 
> color += BLACK_BODY_COLOR(t_coord) * star_color.r * STAR_BRIGHTNESS;
> }
> 
> color += galaxy_color(tex_coord, ray_doppler_factor) * GALAXY_BRIGHTNESS;
> }
> 
> gl_FragColor = color*ray_intensity;
> }
> 
> ```
