Skip to content

Instantly share code, notes, and snippets.

@williame
Created July 12, 2012 15:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save williame/3098912 to your computer and use it in GitHub Desktop.
Save williame/3098912 to your computer and use it in GitHub Desktop.
var mandelbulbCanvas = document.getElementById('mandelbulb');
window.requestAnimFrame = (function(callback) {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 100);
};
})();
function animate() {
var context = mandelbulbCanvas.getContext("2d");
context.fillRect(0, 0, mandelbulbCanvas.width, mandelbulbCanvas.height);
var image = context.getImageData(0, 0, mandelbulbCanvas.width, mandelbulbCanvas.height);
var imageData = image.data;
render(image, imageData, context);
}
window.onload = function() {
animate();
};
var scale = 140.0;
var mapZ = new FastVec3(0.0, 0.0, 0.0);
function map(z) {
mapZ.setTo(z);
mapZ.scalarMultiply(1/scale);
return mandelbulb(mapZ) * scale;
}
function dotProduct(v1, v2) {
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
function length(otherVec) {
var part1 = (otherVec.x) * (otherVec.x);
var part2 = (otherVec.y) * (otherVec.y);
var part3 = (otherVec.z) * (otherVec.z);
var underRadical = part1 + part2 + part3;
return Math.sqrt(underRadical);
}
function toRad(r) {
return r * Math.PI / 180.0;
}
function saturate(n) {
return clamp(n, 0.0, 1.0);
}
function clamp(n, min, max) {
return Math.max(min, Math.min(n, max));
}
function render(image, imageData, context) {
var rad = toRad(lightAngle);
var lightX = ((Math.cos(rad) * (DEPTH_OF_FIELD/2)));
var lightZ = ((Math.sin(rad) * (DEPTH_OF_FIELD/2)));
lightLocation.x = lightX;
lightLocation.y = 40.0;
lightLocation.z = lightZ;
lightDirection.x = -lightLocation.x;
lightDirection.y = -lightLocation.y;
lightDirection.z = -lightLocation.z;
lightDirection.normalize();
var viewRad = toRad(viewAngle);
var viewX = ((Math.cos(viewRad) * (DEPTH_OF_FIELD/2)));
var viewZ = ((Math.sin(viewRad) * (DEPTH_OF_FIELD/2)));
nearFieldLocation.x = viewX;
nearFieldLocation.y = -20.0;
nearFieldLocation.z = viewZ;
viewDirection.x = -nearFieldLocation.x;
viewDirection.y = -nearFieldLocation.y;
viewDirection.z = -nearFieldLocation.z;
viewDirection.normalize();
//Place eye:
var eyeDistanceFromNearField = 2000.0;
reverseDirection.setTo(viewDirection);
reverseDirection.scalarMultiply(eyeDistanceFromNearField);
eyeLocation.setTo(nearFieldLocation);
eyeLocation.subtract(reverseDirection);
var y = 0;
var scanline = function() {
console.log("scanline "+y);
for(var x=0; x<mandelbulbCanvas.width; x++) {
var nx = x - (mandelbulbCanvas.width/2.0);
var ny = y - (mandelbulbCanvas.height/2.0);
pixelLocation.setTo(nearFieldLocation);
tempViewDirection.setTo(viewDirection);
tempViewDirection.turnOrthogonal();
tempViewDirection.scalarMultiply(nx);
pixelLocation.add(tempViewDirection);
tempViewDirection.setTo(viewDirection);
tempViewDirection.turnOrthogonal();
tempViewDirection.crossProduct(viewDirection);
tempViewDirection.scalarMultiply(ny);
pixelLocation.add(tempViewDirection);
rayLocation.setTo(pixelLocation);
rayDirection.setTo(rayLocation);
rayDirection.subtract(eyeLocation);
rayDirection.normalize();
var distanceFromCamera = 0.0;
var d = map(rayLocation);
/*console.log(rayLocation.x);
console.log(rayLocation.y);
console.log(rayLocation.z);
console.log('--');*/
var iterations = 0;
for(; iterations < MAX_ITER; iterations++) {
if(d < halfPixel) {
break;
}
//Increase rayLocation with direction and d:
rayDirection.scalarMultiply(d);
rayLocation.add(rayDirection);
rayDirection.normalize();
//Move the pixel location:
temp.setTo(nearFieldLocation);
temp.subtract(rayLocation);
distanceFromCamera = length(temp);
if(distanceFromCamera > DEPTH_OF_FIELD) {
break;
}
d = map(rayLocation);
}
if(distanceFromCamera < DEPTH_OF_FIELD) {
rayLocation.subtract(smallX);
var locationMinX = map(rayLocation);
rayLocation.add(bigX);
var locationPlusX = map(rayLocation);
rayLocation.subtract(smallX);
rayLocation.subtract(smallY);
var locationMinY = map(rayLocation);
rayLocation.add(bigY);
var locationPlusY = map(rayLocation);
rayLocation.subtract(smallY);
rayLocation.subtract(smallZ);
var locationMinZ = map(rayLocation);
rayLocation.add(bigZ);
var locationPlusZ = map(rayLocation);
rayLocation.subtract(smallZ);
//Calculate the normal:
normal.x = (locationMinX - locationPlusX);
normal.y = (locationMinY - locationPlusY);
normal.z = (locationMinZ - locationPlusZ);
normal.normalize();
//Calculate the ambient light:
var dotNL = dotProduct(lightDirection, normal);
var diff = saturate(dotNL);
//Calculate specular light:
halfway.setTo(rayDirection);
halfway.add(lightDirection);
halfway.normalize();
var dotNH = dotProduct(halfway, normal);
var spec = Math.pow(saturate(dotNH),35);
var shad = shadow(1.0, DEPTH_OF_FIELD, 16.0);
var brightness = (10.0 + (200.0 + spec * 45.0) * shad * diff) / 255.0;
var red = 10+(380 * brightness);
var green = 10+(280 * brightness);
var blue = (180 * brightness);
red = clamp(red, 0, 255.0);
green = clamp(green, 0, 255.0);
blue = clamp(blue, 0, 255.0);
var pixels = (y*mandelbulbCanvas.width) + x;
imageData[4*pixels+0] = red;
imageData[4*pixels+1] = green;
imageData[4*pixels+2] = blue;
imageData[4*pixels+3] = 255;
} else {
var pixels = (y*mandelbulbCanvas.width) + x;
imageData[4*pixels+0] = 155+clamp(iterations*1.5, 0.0, 100.0);
imageData[4*pixels+1] = 205+clamp(iterations*1.5, 0.0, 50.0);
imageData[4*pixels+2] = 255;
imageData[4*pixels+3] = 255;
}
}
image.data = imageData;
context.putImageData(image, 0, 0);
if(++y<mandelbulbCanvas.height)
setTimeout(scanline,0);
}
setTimeout(scanline,0)
}
var halfPixel = Math.sqrt(2.0)/2.0;
var MAX_ITER = 5000.0;
var DEPTH_OF_FIELD = 1000.0;
var lightAngle = 140.0;
var viewAngle = 150.0;
var lightLocation = new FastVec3(0.0, 0.0, 0.0);
var lightDirection = new FastVec3(0.0, 0.0, 0.0);
var nearFieldLocation = new FastVec3(0.0, 0.0, 0.0);
var viewDirection = new FastVec3(0.0, 0.0, 0.0);
var reverseDirection = new FastVec3(0.0, 0.0, 0.0);
var eyeLocation = new FastVec3(0.0, 0.0, 0.0);
var pixelLocation = new FastVec3(0.0, 0.0, 0.0);
var rayLocation = new FastVec3(0.0, 0.0, 0.0);
var tempViewDirection = new FastVec3(0.0, 0.0, 0.0);
var rayDirection = new FastVec3(0.0, 0.0, 0.0);
var normal = new FastVec3(0.0, 0.0, 0.0);
var halfway = new FastVec3(0.0, 0.0, 0.0);
var smallX = new FastVec3(0.01,0,0);
var smallY = new FastVec3(0,0.01,0);
var smallZ = new FastVec3(0.0, 0.0, 0.01);
var bigX = new FastVec3(0.02,0,0);
var bigY = new FastVec3(0,0.02,0);
var bigZ = new FastVec3(0.0, 0.0, 0.02);
var temp = new FastVec3(0.0, 0.0, 0.0);
var ro = new FastVec3(0.0, 0.0, 0.0);
var rd = new FastVec3(0.0, 0.0, 0.0);
function shadow(mint, maxt, k) {
var res = 1.0;
for(var t=mint; t < maxt; ) {
rd.setTo(lightDirection);
rd.scalarMultiply(t);
ro.setTo(rayLocation);
ro.subtract(rd);
var h = map(ro);
if( h < 0.001) {
return 0.0;
}
res = Math.min( res, k*h/t );
t += h;
}
return res;
}
var z = new FastVec3(0.0, 0.0, 0.0);
/**
* The mandelbulb from:
* http://blog.hvidtfeldts.net/index.php/2011/09/distance-estimated-3d-fractals-v-the-mandelbulb-different-de-approximations/
*/
var Iterations = 20.0;
var Power = 8.0;
function mandelbulb(pos) {
z.setTo(pos);
var dr = 1.0;
var r = 0.0;
for (var i = 0; i < Iterations ; i++) {
r = length(z);
if (r>DEPTH_OF_FIELD) break;
var theta = Math.acos(z.z/r);
var phi = Math.atan2(z.y,z.x);
dr = Math.pow( r, Power-1.0)*Power*dr + 1.0;
var zr = Math.pow( r,Power);
theta = theta*Power;
phi = phi*Power;
z.x = Math.sin(theta)*Math.cos(phi);
z.y = Math.sin(phi)*Math.sin(theta);
z.z = Math.cos(theta);
z.scalarMultiply(zr);
z.add(pos);
}
return 0.5*Math.log(r)*r/dr;
}
function animateCamera() {
lightAngle -= 1;
lightAngle %= 360.0;
viewAngle -= 1;
viewAngle %= 360.0;
}
function FastVec3(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
this.getNorm = function() {
return Math.sqrt (this.x * this.x + this.y * this.y + this.z * this.z);
}
this.normalize = function() {
this.scalarMultiply(1 / this.getNorm());
}
this.scalarMultiply = function(amount) {
this.x *= amount;
this.y *= amount;
this.z *= amount;
}
this.add = function(v1) {
this.x += v1.x;
this.y += v1.y;
this.z += v1.z;
}
this.subtract = function(v1) {
this.x -= v1.x;
this.y -= v1.y;
this.z -= v1.z;
}
this.setTo = function(toMirror) {
this.x = toMirror.x;
this.y = toMirror.y;
this.z = toMirror.z;
}
this.turnOrthogonal = function() {
var inverse = 1.0 / Math.sqrt(this.x * this.x + this.z * this.z);
var oldX = this.x;
this.x = -inverse * this.z;
this.z = inverse * oldX;
}
this.crossProduct = function(v1) {
var oldX = this.x;
var oldY = this.y;
var oldZ = this.z;
this.x = v1.y * oldZ - v1.z * oldY;
this.y = v1.z * oldX - v1.x * oldZ;
this.z = v1.x * oldY - v1.y * oldX;
}
this.clamp = function(i, min, max) {
return Math.max(min, Math.min(i, max));
}
this.clamp = function(min, max) {
this.x = clamp(this.x, min, max);
this.y = clamp(this.y, min, max);
this.z = clamp(this.z, min, max);
}
}
@FrankFang
Copy link

Is there a demo?

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