/* WebGL Mandelbox by Peter Nitsch http://www.peternitsch.net
 * credits / glsl fragment shader by Rrrola, Mandelbox formula by Tom Lowe 
 * http://www.fractalforums.com/3d-fractal-generation/amazing-fractal
 */

const rotUnitToAngle = Math.PI / 180;
const fov = (75 *3.14159265358979/180);

var shaders = {};
var gl, canvas, sp, v;
var mvUniform, tex0Uniform, rotUniform, pUniform;

var key = Object();
var keyListener = null;
var keyIsDown;

var rotMatrix = $M([[1, 0, 0], [0, 0, -1.0], [0, 1.0, 0]]);

var pMatrix;
var fovX, fovY;

var a1, u2, u3, u4, u5, u6;

var scaleElement, iterElement, stepsElement;
var mouseButton = false;
var newMouseX = 0;
var newMouseY = 0;
var lastMouseX = 0;
var lastMouseY = 0;

var pitch = 0;
var pitchRate = 0;
var yaw = 0;
var yawRate = 0;
var camX = 0.0;
var camY = 11.0;
var camZ = 0.0;
var spd = 0.0;
var spdRate = 0.15;
var iters = 9;	
var minIters = 4;
var maxIters=9;
var scale=2.8; 
var maxSteps=140;
var lastIters = 4;
var lastScale = 2.4;
var lastMaxSteps = 140;

var vShader = [
	"attribute vec2 pos;",

	"uniform mat4 uMVMatrix;",
	"uniform mat4 uPMatrix;",
	"uniform mat4 uRotMatrix;",

	"varying vec3 eye, dir;",

	"uniform float fovX, fovY;",  

	"float fov2scale(float fov) { return tan(fov/2.0); }",

	"void main(void) {",
	"	gl_Position = vec4(pos.x,pos.y,0.0,1.0);",
	"	eye = vec3(uMVMatrix[3]);",
	"  	dir = vec3(uPMatrix*uMVMatrix*uRotMatrix*vec4(fov2scale(fovX)*gl_Position.x, fov2scale(fovY)*gl_Position.y, 1, 0) );",
	"}"
    ].join("\n");




var fShader = [
	"// Mandelbox shader by Rrrola",
	"// Original formula by Tglad",
	"// - http://www.fractalforums.com/3d-fractal-generation/amazing-fractal",

	"#ifdef GL_ES",
	"precision highp float;",
	"#endif",

	"#define P0 p0                    // standard Mandelbox",

	"#define SCALE 2.8",
	"#define MINRAD2 -1.77",

	"#define DIST_MULTIPLIER 1.0",
	"#define MAX_DIST 64.0",

	"varying vec3 eye, dir;",

	"//uniform vec2 par[10];",

	"float",
	"  min_dist = 0.01,           // Distance at which raymarching stops.",
	"  ao_eps = 0.00015,             // Base distance at which ambient occlusion is estimated.",
	"  ao_strength = 0.1,        // Strength of ambient occlusion.",
	"  glow_strength = 0.35,      // How much glow is applied after maxSteps.",
	"  dist_to_color = 0.1;      // How is background mixed with the surface color after maxSteps.",

	"uniform int iters;    // Number of fractal iterations.",
	//" // color_iters = 9,        // Number of fractal iterations for coloring.",
	"uniform int maxSteps;          // Maximum raymarching steps.",

	"int color_iters = 9;",
	
	"vec3 backgroundColor = vec3(0.0, 0.0, 0.0),",
	"  surfaceColor1 = vec3(0.15, 0.64, 0.91),",
	"  surfaceColor2 = vec3(0.9, 0.9, 0.9),",
	"  surfaceColor3 = vec3(0.15, 0.55, 0.55),",
	"  specularColor = vec3(1.0, 1.0, 0.5),",
	"  glowColor = vec3(0.3, 0.1, 0.3),",
	"  aoColor = vec3(1.0, 0, 0);",

	"float minRad2 = clamp(MINRAD2, 1.0e-9, 1.0);",
	"vec4 scale = vec4(SCALE, SCALE, SCALE, abs(SCALE)) / minRad2;",
	"float absScalem1 = abs(SCALE - 1.0);",
	"float AbsScaleRaisedTo1mIters = pow(abs(SCALE), float(1-iters));",

	"float d(vec3 pos) {",
	"  vec4 p = vec4(pos,1), p0 = p;  // p.w is the distance estimate",

	"  for (int i=0; i<9; i++) {",
	"    p.xyz = clamp(p.xyz, -1.0, 1.0) * 2.0 - p.xyz;  // min;max;mad",

	"    float r2 = dot(p.xyz, p.xyz);",
	"    p *= clamp(max(minRad2/r2, minRad2), 0.0, 1.0);  // dp3,div,max.sat,mul",
	"    p = p*scale + P0;",
	"  }",
	"  return ((length(p.xyz) - absScalem1) / p.w - AbsScaleRaisedTo1mIters) * DIST_MULTIPLIER;",
	"}",


	"vec3 color(vec3 pos) {",
	"  vec3 p = pos, p0 = p;",
	"  float trap = 1.0;",

	"  for (int i=0; i<9; i++) {",
	"    p.xyz = clamp(p.xyz, -1.0, 1.0) * 2.0 - p.xyz;",
	"    float r2 = dot(p.xyz, p.xyz);",
	"    p *= clamp(max(minRad2/r2, minRad2), 0.0, 1.0);",
	"    p = p*scale.xyz + P0.xyz;",
	"    trap = min(trap, r2);",
	"  }",
	"  vec2 c = clamp(vec2( 0.33*log(dot(p,p))-1.0, sqrt(trap) ), 0.0, 1.0);",

	"  return mix(mix(surfaceColor1, surfaceColor2, c.y), surfaceColor3, c.x);",
	"}",


	"float normal_eps = 0.00001;",
	"vec3 normal(vec3 pos, float d_pos) {",
	"  vec4 Eps = vec4(0, normal_eps, 2.0*normal_eps, 3.0*normal_eps);",
	"  return normalize(vec3(",
	"  // 2-tap forward differences, error = O(eps)",
	"//    -d_pos+d(pos+Eps.yxx),",
	"//    -d_pos+d(pos+Eps.xyx),",
	"//    -d_pos+d(pos+Eps.xxy)",

	"  // 3-tap central differences, error = O(eps^2)",
	"    -d(pos-Eps.yxx)+d(pos+Eps.yxx),",
	"    -d(pos-Eps.xyx)+d(pos+Eps.xyx),",
	"    -d(pos-Eps.xxy)+d(pos+Eps.xxy)",

	"  // 4-tap forward differences, error = O(eps^3)",
	"//    -2.0*d(pos-Eps.yxx)-3.0*d_pos+6.0*d(pos+Eps.yxx)-d(pos+Eps.zxx),",
	"//    -2.0*d(pos-Eps.xyx)-3.0*d_pos+6.0*d(pos+Eps.xyx)-d(pos+Eps.xzx),",
	"//    -2.0*d(pos-Eps.xxy)-3.0*d_pos+6.0*d(pos+Eps.xxy)-d(pos+Eps.xxz)",

	"  // 5-tap central differences, error = O(eps^4)",
	"//    d(pos-Eps.zxx)-8.0*d(pos-Eps.yxx)+8.0*d(pos+Eps.yxx)-d(pos+Eps.zxx),",
	"//    d(pos-Eps.xzx)-8.0*d(pos-Eps.xyx)+8.0*d(pos+Eps.xyx)-d(pos+Eps.xzx),",
	"//    d(pos-Eps.xxz)-8.0*d(pos-Eps.xxy)+8.0*d(pos+Eps.xxy)-d(pos+Eps.xxz)",
	"  ));",
	"}",


	"vec3 blinn_phong(vec3 normal, vec3 view, vec3 light, vec3 diffuseColor) {",
	"  vec3 halfLV = normalize(light + view);",
	"  float spe = pow(max( dot(normal, halfLV), 0.0 ), 32.0);",
	"  float dif = dot(normal, light) * 0.5 + 0.75;",
	"  return dif*diffuseColor + spe*specularColor;",
	"}",

	"float ambient_occlusion(vec3 p, vec3 n) {",
	"  float ao = 1.0, w = ao_strength/ao_eps;",
	"  float dist = 2.0 * ao_eps;",

	"  for (int i=0; i<5; i++) {",
	"    float D = d(p + n*dist);",
	"    ao -= (dist-D) * w;",
	"    w *= 0.5;",
	"    dist = dist*2.0 - ao_eps;  // 2,3,5,9,17",
	"  }",
	"  return clamp(ao, 0.0, 1.0);",
	"}",


	"void main() {",
	"  vec3 p = eye, dp = normalize(dir);",

	"  float totalD = 0.0, D = 3.4e38, extraD = 0.0, lastD;",

//	"  int steps;",
	"  for (int steps=0; steps<140; steps++) {",
	"    lastD = D;",
	"    D = d(p + totalD * dp);",

	"    if (extraD > 0.0 && D < extraD) {",
	"      totalD -= extraD;",
	"      extraD = 0.0;",
	"      D = 3.4e38;",
//	"      steps--;",
	"      continue;",
	"    }",

	"    if (D < min_dist || D > MAX_DIST) break;",

	"    totalD += D;",

	"    totalD += extraD = 0.096 * D*(D+extraD)/lastD;",
	"  }",

	"  p += totalD * dp;",

	"  vec3 col = backgroundColor;",

	"  if (D < MAX_DIST) {",
	"    vec3 n = normal(p, D);",
	"    col = color(p);",
	"    col = blinn_phong(n, -dp, normalize(eye+vec3(0,1,0)+dp), col);",
	"    col = mix(aoColor, col, ambient_occlusion(p, n));",

	"    if (D > min_dist) {",
	"      col = mix(col, backgroundColor, clamp(log(D/min_dist) * dist_to_color, 0.0, 1.0));",
	"    }",
	"  }",

	"  col = mix(col, glowColor, float(140)/float(maxSteps) * glow_strength);",
	"  gl_FragColor = vec4(col, 1);",
	"}"
	].join("\n");


function perspective(fovy, aspect, znear, zfar) {
	pMatrix = makePerspective(fovy, aspect, znear, zfar)
}

function multMatrix(m) {
	mvMatrix = mvMatrix.x(m);
}

function setMatrixParam(param, matrix) {
	gl.uniformMatrix4fv(param, false, new Float32Array(matrix.make4x4().flatten()));
}

function doRotate(units, v) {
	var angle = units * rotUnitToAngle;
	v = rotMatrix.inv().x($V(v));
	rotMatrix = rotMatrix.x(Matrix.Rotation(angle, v));
	setMatrixParam(rotUniform, rotMatrix);
}

function init() {
	canvas = document.getElementById("canvas");
	gl = canvas.getContext("experimental-webgl");
	
	fovX = Math.atan(Math.tan(fov/2)*canvas.width/canvas.height)*2;
	fovY = fov;
	
	if (!("sp" in shaders)) {
		shaders.vs = gl.createShader(gl.VERTEX_SHADER);
	    shaders.fs = gl.createShader(gl.FRAGMENT_SHADER);

	    gl.shaderSource(shaders.vs, vShader);
	    gl.shaderSource(shaders.fs, fShader);

	    gl.compileShader(shaders.vs);
	    gl.compileShader(shaders.fs);

		shaders.sp = gl.createProgram();
		gl.attachShader(shaders.sp, shaders.vs);
		gl.attachShader(shaders.sp, shaders.fs);
		gl.linkProgram(shaders.sp);

		if (!gl.getProgramParameter(shaders.sp, gl.LINK_STATUS)) {
			alert(gl.getProgramInfoLog(shaders.sp));
		}

		gl.useProgram(shaders.sp);
	}

	sp = shaders.sp;

	pUniform = gl.getUniformLocation(sp, "uPMatrix");
	mvUniform = gl.getUniformLocation(sp, "uMVMatrix");
	rotUniform = gl.getUniformLocation(sp, "uRotMatrix");	
	
	a1 = gl.getAttribLocation(shaders.sp, "pos");
	u2 = gl.getUniformLocation(shaders.sp, "fovX");
	u3 = gl.getUniformLocation(shaders.sp, "fovY");
	u4 = gl.getUniformLocation(shaders.sp, "iters");
//	u5 = gl.getUniformLocation(shaders.sp, "scale");
	u6 = gl.getUniformLocation(shaders.sp, "maxSteps");		
}

function initUI() {
	scaleElement = document.getElementById("scale");
	
	scale = scaleElement.value;
	
	iterElement = document.getElementById("iterations");
	maxIters = iterElement.value;
	
	stepsElement = document.getElementById("maxSteps");
	maxSteps = stepsElement.value;
	
	scaleElement.addEventListener("change", function(ev) {
		scale = scaleElement.value;
		return true;
	}, false);

	iterElement.addEventListener("change", function(ev) {
		maxIters = iterElement.value;
		return true;
	}, false);
	
	stepsElement.addEventListener("change", function(ev) {
		maxSteps = stepsElement.value;
		return true;
	}, false);
}

function initMouse() {
	canvas.addEventListener("mousedown", function(ev) {
		mouseButton = true;
		return true;
	}, false);

	canvas.addEventListener("mousemove", function(ev) {
		newMouseX = ev.clientX;
		newMouseY = ev.clientY;

		if (!mouseButton) {
			lastMouseX = ev.clientX;
			lastMouseY = ev.clientY;
		}
		return true;
	}, false);

	canvas.addEventListener("mouseup", function(ev) {
		mouseButton = false;
	}, false);

	canvas.addEventListener("mouseout", function(ev) {
		mouseButton = false;
	}, false);
}

function initKeyboard() {
	window.addEventListener("keydown", function(e) {
		key[e.keyCode] = true;
	}, false);
	
	window.addEventListener("keyup", function(e) {
		key[e.keyCode] = false;
	}, false);
}

function renderStart() {
	init();
	initMouse();
	initKeyboard();
	//initUI();

    var vertices = new Float32Array([ -1., -1.,   1., -1.,    -1.,  1.,     1., -1.,    1.,  1.,    -1.,  1.]);

    v = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, v);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

	setMatrixParam(rotUniform, rotMatrix);
	perspective(100, 1, 0.1, 100.0);

	setInterval(function() { draw(); }, 100);
}

function draw() {
	iters = maxIters;

	if (key[38] || key[87]) {
		spd = spdRate;
		iters = minIters;
	} else if (key[40] || key[83]) {
		spd = -spdRate;
		iters = minIters;
	} else {
		spd = 0;
	}

	loadIdentity();
	pushMatrix();
	mvRotate(pitch, [1, 0, 0]);
	mvRotate(yaw, [0, 0, 1]);
	mvTranslate([camX, camY, camZ]);
	
	var mDiffY = lastMouseY - newMouseY;
	var mDiffX = lastMouseX - newMouseX;
	yaw += yawRate;
	pitch += pitchRate;

	if (mouseButton) {
		if ( mDiffY != 0 ) 
			camZ += mDiffY / 120; 
		if ( mDiffX != 0 )   
			camX += mDiffX / 120; 
		iters = minIters;
	}

	if (spd != 0) {
		camX += Math.sin(yaw / 180 * Math.PI) * spd;
		camY += -Math.cos(yaw / 180 * Math.PI) * spd;
	}

	if(lastIters != maxIters || lastScale != scale || lastMaxSteps != maxSteps) {
		setMatrixParam(pUniform, pMatrix);
		setMatrixParam(mvUniform, mvMatrix);
		
		gl.viewport(0, 0, canvas.width, canvas.height);
		gl.clear(gl.COLOR_BUFFER_BIT);
		gl.useProgram(shaders.sp);
		gl.bindBuffer(gl.ARRAY_BUFFER, v);
		gl.uniform1f(u2, fovX);
		gl.uniform1f(u3, fovY);
		gl.uniform1i(u4, iters);
		gl.uniform1f(u5, scale);
		gl.uniform1i(u6, maxSteps);
		gl.vertexAttribPointer(a1, 2, gl.FLOAT, false, 0, 0);
		gl.enableVertexAttribArray(a1);
		gl.drawArrays(gl.TRIANGLES, 0, 6);
	    gl.disableVertexAttribArray(a1);
	}
	
	lastMouseX = newMouseX;
	lastMouseY = newMouseY;
	lastIters = iters;
	lastScale = scale;
	lastMaxSteps = maxSteps;
	
}
