mirror of
https://github.com/tengge1/ShadowEditor.git
synced 2026-01-25 15:08:11 +00:00
1246 lines
43 KiB
JavaScript
1246 lines
43 KiB
JavaScript
/*
|
|
WebGL Path Tracing (http://madebyevan.com/webgl-path-tracing/)
|
|
License: MIT License (see below)
|
|
|
|
Copyright (c) 2010 Evan Wallace
|
|
|
|
Permission is hereby granted, free of charge, to any person
|
|
obtaining a copy of this software and associated documentation
|
|
files (the "Software"), to deal in the Software without
|
|
restriction, including without limitation the rights to use,
|
|
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following
|
|
conditions:
|
|
|
|
The above copyright notice and this permission notice shall be
|
|
included in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// shader strings
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// vertex shader for drawing a textured quad
|
|
var renderVertexSource =
|
|
' attribute vec3 vertex;' +
|
|
' varying vec2 texCoord;' +
|
|
' void main() {' +
|
|
' texCoord = vertex.xy * 0.5 + 0.5;' +
|
|
' gl_Position = vec4(vertex, 1.0);' +
|
|
' }';
|
|
|
|
// fragment shader for drawing a textured quad
|
|
var renderFragmentSource =
|
|
' precision highp float;' +
|
|
' varying vec2 texCoord;' +
|
|
' uniform sampler2D texture;' +
|
|
' void main() {' +
|
|
' gl_FragColor = texture2D(texture, texCoord);' +
|
|
' }';
|
|
|
|
// vertex shader for drawing a line
|
|
var lineVertexSource =
|
|
' attribute vec3 vertex;' +
|
|
' uniform vec3 cubeMin;' +
|
|
' uniform vec3 cubeMax;' +
|
|
' uniform mat4 modelviewProjection;' +
|
|
' void main() {' +
|
|
' gl_Position = modelviewProjection * vec4(mix(cubeMin, cubeMax, vertex), 1.0);' +
|
|
' }';
|
|
|
|
// fragment shader for drawing a line
|
|
var lineFragmentSource =
|
|
' precision highp float;' +
|
|
' void main() {' +
|
|
' gl_FragColor = vec4(1.0);' +
|
|
' }';
|
|
|
|
// constants for the shaders
|
|
var bounces = '5';
|
|
var epsilon = '0.0001';
|
|
var infinity = '10000.0';
|
|
var lightSize = 0.1;
|
|
var lightVal = 0.5;
|
|
|
|
// vertex shader, interpolate ray per-pixel
|
|
var tracerVertexSource =
|
|
' attribute vec3 vertex;' +
|
|
' uniform vec3 eye, ray00, ray01, ray10, ray11;' +
|
|
' varying vec3 initialRay;' +
|
|
' void main() {' +
|
|
' vec2 percent = vertex.xy * 0.5 + 0.5;' +
|
|
' initialRay = mix(mix(ray00, ray01, percent.y), mix(ray10, ray11, percent.y), percent.x);' +
|
|
' gl_Position = vec4(vertex, 1.0);' +
|
|
' }';
|
|
|
|
// start of fragment shader
|
|
var tracerFragmentSourceHeader =
|
|
' precision highp float;' +
|
|
' uniform vec3 eye;' +
|
|
' varying vec3 initialRay;' +
|
|
' uniform float textureWeight;' +
|
|
' uniform float timeSinceStart;' +
|
|
' uniform sampler2D texture;' +
|
|
' uniform float glossiness;' +
|
|
' vec3 roomCubeMin = vec3(-1.0, -1.0, -1.0);' +
|
|
' vec3 roomCubeMax = vec3(1.0, 1.0, 1.0);';
|
|
|
|
// compute the near and far intersections of the cube (stored in the x and y components) using the slab method
|
|
// no intersection means vec.x > vec.y (really tNear > tFar)
|
|
var intersectCubeSource =
|
|
' vec2 intersectCube(vec3 origin, vec3 ray, vec3 cubeMin, vec3 cubeMax) {' +
|
|
' vec3 tMin = (cubeMin - origin) / ray;' +
|
|
' vec3 tMax = (cubeMax - origin) / ray;' +
|
|
' vec3 t1 = min(tMin, tMax);' +
|
|
' vec3 t2 = max(tMin, tMax);' +
|
|
' float tNear = max(max(t1.x, t1.y), t1.z);' +
|
|
' float tFar = min(min(t2.x, t2.y), t2.z);' +
|
|
' return vec2(tNear, tFar);' +
|
|
' }';
|
|
|
|
// given that hit is a point on the cube, what is the surface normal?
|
|
// TODO: do this with fewer branches
|
|
var normalForCubeSource =
|
|
' vec3 normalForCube(vec3 hit, vec3 cubeMin, vec3 cubeMax)' +
|
|
' {' +
|
|
' if(hit.x < cubeMin.x + ' + epsilon + ') return vec3(-1.0, 0.0, 0.0);' +
|
|
' else if(hit.x > cubeMax.x - ' + epsilon + ') return vec3(1.0, 0.0, 0.0);' +
|
|
' else if(hit.y < cubeMin.y + ' + epsilon + ') return vec3(0.0, -1.0, 0.0);' +
|
|
' else if(hit.y > cubeMax.y - ' + epsilon + ') return vec3(0.0, 1.0, 0.0);' +
|
|
' else if(hit.z < cubeMin.z + ' + epsilon + ') return vec3(0.0, 0.0, -1.0);' +
|
|
' else return vec3(0.0, 0.0, 1.0);' +
|
|
' }';
|
|
|
|
// compute the near intersection of a sphere
|
|
// no intersection returns a value of +infinity
|
|
var intersectSphereSource =
|
|
' float intersectSphere(vec3 origin, vec3 ray, vec3 sphereCenter, float sphereRadius) {' +
|
|
' vec3 toSphere = origin - sphereCenter;' +
|
|
' float a = dot(ray, ray);' +
|
|
' float b = 2.0 * dot(toSphere, ray);' +
|
|
' float c = dot(toSphere, toSphere) - sphereRadius*sphereRadius;' +
|
|
' float discriminant = b*b - 4.0*a*c;' +
|
|
' if(discriminant > 0.0) {' +
|
|
' float t = (-b - sqrt(discriminant)) / (2.0 * a);' +
|
|
' if(t > 0.0) return t;' +
|
|
' }' +
|
|
' return ' + infinity + ';' +
|
|
' }';
|
|
|
|
// given that hit is a point on the sphere, what is the surface normal?
|
|
var normalForSphereSource =
|
|
' vec3 normalForSphere(vec3 hit, vec3 sphereCenter, float sphereRadius) {' +
|
|
' return (hit - sphereCenter) / sphereRadius;' +
|
|
' }';
|
|
|
|
// use the fragment position for randomness
|
|
var randomSource =
|
|
' float random(vec3 scale, float seed) {' +
|
|
' return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed);' +
|
|
' }';
|
|
|
|
// random cosine-weighted distributed vector
|
|
// from http://www.rorydriscoll.com/2009/01/07/better-sampling/
|
|
var cosineWeightedDirectionSource =
|
|
' vec3 cosineWeightedDirection(float seed, vec3 normal) {' +
|
|
' float u = random(vec3(12.9898, 78.233, 151.7182), seed);' +
|
|
' float v = random(vec3(63.7264, 10.873, 623.6736), seed);' +
|
|
' float r = sqrt(u);' +
|
|
' float angle = 6.283185307179586 * v;' +
|
|
// compute basis from normal
|
|
' vec3 sdir, tdir;' +
|
|
' if (abs(normal.x)<.5) {' +
|
|
' sdir = cross(normal, vec3(1,0,0));' +
|
|
' } else {' +
|
|
' sdir = cross(normal, vec3(0,1,0));' +
|
|
' }' +
|
|
' tdir = cross(normal, sdir);' +
|
|
' return r*cos(angle)*sdir + r*sin(angle)*tdir + sqrt(1.-u)*normal;' +
|
|
' }';
|
|
|
|
// random normalized vector
|
|
var uniformlyRandomDirectionSource =
|
|
' vec3 uniformlyRandomDirection(float seed) {' +
|
|
' float u = random(vec3(12.9898, 78.233, 151.7182), seed);' +
|
|
' float v = random(vec3(63.7264, 10.873, 623.6736), seed);' +
|
|
' float z = 1.0 - 2.0 * u;' +
|
|
' float r = sqrt(1.0 - z * z);' +
|
|
' float angle = 6.283185307179586 * v;' +
|
|
' return vec3(r * cos(angle), r * sin(angle), z);' +
|
|
' }';
|
|
|
|
// random vector in the unit sphere
|
|
// note: this is probably not statistically uniform, saw raising to 1/3 power somewhere but that looks wrong?
|
|
var uniformlyRandomVectorSource =
|
|
' vec3 uniformlyRandomVector(float seed) {' +
|
|
' return uniformlyRandomDirection(seed) * sqrt(random(vec3(36.7539, 50.3658, 306.2759), seed));' +
|
|
' }';
|
|
|
|
// compute specular lighting contribution
|
|
var specularReflection =
|
|
' vec3 reflectedLight = normalize(reflect(light - hit, normal));' +
|
|
' specularHighlight = max(0.0, dot(reflectedLight, normalize(hit - origin)));';
|
|
|
|
// update ray using normal and bounce according to a diffuse reflection
|
|
var newDiffuseRay =
|
|
' ray = cosineWeightedDirection(timeSinceStart + float(bounce), normal);';
|
|
|
|
// update ray using normal according to a specular reflection
|
|
var newReflectiveRay =
|
|
' ray = reflect(ray, normal);' +
|
|
specularReflection +
|
|
' specularHighlight = 2.0 * pow(specularHighlight, 20.0);';
|
|
|
|
// update ray using normal and bounce according to a glossy reflection
|
|
var newGlossyRay =
|
|
' ray = normalize(reflect(ray, normal)) + uniformlyRandomVector(timeSinceStart + float(bounce)) * glossiness;' +
|
|
specularReflection +
|
|
' specularHighlight = pow(specularHighlight, 3.0);';
|
|
|
|
var yellowBlueCornellBox =
|
|
' if(hit.x < -0.9999) surfaceColor = vec3(0.1, 0.5, 1.0);' + // blue
|
|
' else if(hit.x > 0.9999) surfaceColor = vec3(1.0, 0.9, 0.1);'; // yellow
|
|
|
|
var redGreenCornellBox =
|
|
' if(hit.x < -0.9999) surfaceColor = vec3(1.0, 0.3, 0.1);' + // red
|
|
' else if(hit.x > 0.9999) surfaceColor = vec3(0.3, 1.0, 0.1);'; // green
|
|
|
|
function makeShadow(objects) {
|
|
return '' +
|
|
' float shadow(vec3 origin, vec3 ray) {' +
|
|
concat(objects, function (o) { return o.getShadowTestCode(); }) +
|
|
' return 1.0;' +
|
|
' }';
|
|
}
|
|
|
|
function makeCalculateColor(objects) {
|
|
return '' +
|
|
' vec3 calculateColor(vec3 origin, vec3 ray, vec3 light) {' +
|
|
' vec3 colorMask = vec3(1.0);' +
|
|
' vec3 accumulatedColor = vec3(0.0);' +
|
|
|
|
// main raytracing loop
|
|
' for(int bounce = 0; bounce < ' + bounces + '; bounce++) {' +
|
|
// compute the intersection with everything
|
|
' vec2 tRoom = intersectCube(origin, ray, roomCubeMin, roomCubeMax);' +
|
|
concat(objects, function (o) { return o.getIntersectCode(); }) +
|
|
|
|
// find the closest intersection
|
|
' float t = ' + infinity + ';' +
|
|
' if(tRoom.x < tRoom.y) t = tRoom.y;' +
|
|
concat(objects, function (o) { return o.getMinimumIntersectCode(); }) +
|
|
|
|
// info about hit
|
|
' vec3 hit = origin + ray * t;' +
|
|
' vec3 surfaceColor = vec3(0.75);' +
|
|
' float specularHighlight = 0.0;' +
|
|
' vec3 normal;' +
|
|
|
|
// calculate the normal (and change wall color)
|
|
' if(t == tRoom.y) {' +
|
|
' normal = -normalForCube(hit, roomCubeMin, roomCubeMax);' +
|
|
[yellowBlueCornellBox, redGreenCornellBox][environment] +
|
|
newDiffuseRay +
|
|
' } else if(t == ' + infinity + ') {' +
|
|
' break;' +
|
|
' } else {' +
|
|
' if(false) ;' + // hack to discard the first 'else' in 'else if'
|
|
concat(objects, function (o) { return o.getNormalCalculationCode(); }) +
|
|
[newDiffuseRay, newReflectiveRay, newGlossyRay][material] +
|
|
' }' +
|
|
|
|
// compute diffuse lighting contribution
|
|
' vec3 toLight = light - hit;' +
|
|
' float diffuse = max(0.0, dot(normalize(toLight), normal));' +
|
|
|
|
// trace a shadow ray to the light
|
|
' float shadowIntensity = shadow(hit + normal * ' + epsilon + ', toLight);' +
|
|
|
|
// do light bounce
|
|
' colorMask *= surfaceColor;' +
|
|
' accumulatedColor += colorMask * (' + lightVal + ' * diffuse * shadowIntensity);' +
|
|
' accumulatedColor += colorMask * specularHighlight * shadowIntensity;' +
|
|
|
|
// calculate next origin
|
|
' origin = hit;' +
|
|
' }' +
|
|
|
|
' return accumulatedColor;' +
|
|
' }';
|
|
}
|
|
|
|
function makeMain() {
|
|
return '' +
|
|
' void main() {' +
|
|
' vec3 newLight = light + uniformlyRandomVector(timeSinceStart - 53.0) * ' + lightSize + ';' +
|
|
' vec3 texture = texture2D(texture, gl_FragCoord.xy / 512.0).rgb;' +
|
|
' gl_FragColor = vec4(mix(calculateColor(eye, initialRay, newLight), texture, textureWeight), 1.0);' +
|
|
' }';
|
|
}
|
|
|
|
function makeTracerFragmentSource(objects) {
|
|
return tracerFragmentSourceHeader +
|
|
concat(objects, function (o) { return o.getGlobalCode(); }) +
|
|
intersectCubeSource +
|
|
normalForCubeSource +
|
|
intersectSphereSource +
|
|
normalForSphereSource +
|
|
randomSource +
|
|
cosineWeightedDirectionSource +
|
|
uniformlyRandomDirectionSource +
|
|
uniformlyRandomVectorSource +
|
|
makeShadow(objects) +
|
|
makeCalculateColor(objects) +
|
|
makeMain();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// utility functions
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function getEyeRay(matrix, x, y) {
|
|
return matrix.multiply(Vector.create([x, y, 0, 1])).divideByW().ensure3().subtract(eye);
|
|
}
|
|
|
|
function setUniforms(program, uniforms) {
|
|
for (var name in uniforms) {
|
|
var value = uniforms[name];
|
|
var location = gl.getUniformLocation(program, name);
|
|
if (location == null) continue;
|
|
if (value instanceof Vector) {
|
|
gl.uniform3fv(location, new Float32Array([value.elements[0], value.elements[1], value.elements[2]]));
|
|
} else if (value instanceof Matrix) {
|
|
gl.uniformMatrix4fv(location, false, new Float32Array(value.flatten()));
|
|
} else {
|
|
gl.uniform1f(location, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function concat(objects, func) {
|
|
var text = '';
|
|
for (var i = 0; i < objects.length; i++) {
|
|
text += func(objects[i]);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
Vector.prototype.ensure3 = function () {
|
|
return Vector.create([this.elements[0], this.elements[1], this.elements[2]]);
|
|
};
|
|
|
|
Vector.prototype.ensure4 = function (w) {
|
|
return Vector.create([this.elements[0], this.elements[1], this.elements[2], w]);
|
|
};
|
|
|
|
Vector.prototype.divideByW = function () {
|
|
var w = this.elements[this.elements.length - 1];
|
|
var newElements = [];
|
|
for (var i = 0; i < this.elements.length; i++) {
|
|
newElements.push(this.elements[i] / w);
|
|
}
|
|
return Vector.create(newElements);
|
|
};
|
|
|
|
Vector.prototype.componentDivide = function (vector) {
|
|
if (this.elements.length != vector.elements.length) {
|
|
return null;
|
|
}
|
|
var newElements = [];
|
|
for (var i = 0; i < this.elements.length; i++) {
|
|
newElements.push(this.elements[i] / vector.elements[i]);
|
|
}
|
|
return Vector.create(newElements);
|
|
};
|
|
|
|
Vector.min = function (a, b) {
|
|
if (a.length != b.length) {
|
|
return null;
|
|
}
|
|
var newElements = [];
|
|
for (var i = 0; i < a.elements.length; i++) {
|
|
newElements.push(Math.min(a.elements[i], b.elements[i]));
|
|
}
|
|
return Vector.create(newElements);
|
|
};
|
|
|
|
Vector.max = function (a, b) {
|
|
if (a.length != b.length) {
|
|
return null;
|
|
}
|
|
var newElements = [];
|
|
for (var i = 0; i < a.elements.length; i++) {
|
|
newElements.push(Math.max(a.elements[i], b.elements[i]));
|
|
}
|
|
return Vector.create(newElements);
|
|
};
|
|
|
|
Vector.prototype.minComponent = function () {
|
|
var value = Number.MAX_VALUE;
|
|
for (var i = 0; i < this.elements.length; i++) {
|
|
value = Math.min(value, this.elements[i]);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
Vector.prototype.maxComponent = function () {
|
|
var value = -Number.MAX_VALUE;
|
|
for (var i = 0; i < this.elements.length; i++) {
|
|
value = Math.max(value, this.elements[i]);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
function compileSource(source, type) {
|
|
var shader = gl.createShader(type);
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
throw 'compile error: ' + gl.getShaderInfoLog(shader);
|
|
}
|
|
return shader;
|
|
}
|
|
|
|
function compileShader(vertexSource, fragmentSource) {
|
|
var shaderProgram = gl.createProgram();
|
|
gl.attachShader(shaderProgram, compileSource(vertexSource, gl.VERTEX_SHADER));
|
|
gl.attachShader(shaderProgram, compileSource(fragmentSource, gl.FRAGMENT_SHADER));
|
|
gl.linkProgram(shaderProgram);
|
|
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
throw 'link error: ' + gl.getProgramInfoLog(shaderProgram);
|
|
}
|
|
return shaderProgram;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class Sphere
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function Sphere(center, radius, id) {
|
|
this.center = center;
|
|
this.radius = radius;
|
|
this.centerStr = 'sphereCenter' + id;
|
|
this.radiusStr = 'sphereRadius' + id;
|
|
this.intersectStr = 'tSphere' + id;
|
|
this.temporaryTranslation = Vector.create([0, 0, 0]);
|
|
}
|
|
|
|
Sphere.prototype.getGlobalCode = function () {
|
|
return '' +
|
|
' uniform vec3 ' + this.centerStr + ';' +
|
|
' uniform float ' + this.radiusStr + ';';
|
|
};
|
|
|
|
Sphere.prototype.getIntersectCode = function () {
|
|
return '' +
|
|
' float ' + this.intersectStr + ' = intersectSphere(origin, ray, ' + this.centerStr + ', ' + this.radiusStr + ');';
|
|
};
|
|
|
|
Sphere.prototype.getShadowTestCode = function () {
|
|
return '' +
|
|
this.getIntersectCode() +
|
|
' if(' + this.intersectStr + ' < 1.0) return 0.0;';
|
|
};
|
|
|
|
Sphere.prototype.getMinimumIntersectCode = function () {
|
|
return '' +
|
|
' if(' + this.intersectStr + ' < t) t = ' + this.intersectStr + ';';
|
|
};
|
|
|
|
Sphere.prototype.getNormalCalculationCode = function () {
|
|
return '' +
|
|
' else if(t == ' + this.intersectStr + ') normal = normalForSphere(hit, ' + this.centerStr + ', ' + this.radiusStr + ');';
|
|
};
|
|
|
|
Sphere.prototype.setUniforms = function (renderer) {
|
|
renderer.uniforms[this.centerStr] = this.center.add(this.temporaryTranslation);
|
|
renderer.uniforms[this.radiusStr] = this.radius;
|
|
};
|
|
|
|
Sphere.prototype.temporaryTranslate = function (translation) {
|
|
this.temporaryTranslation = translation;
|
|
};
|
|
|
|
Sphere.prototype.translate = function (translation) {
|
|
this.center = this.center.add(translation);
|
|
};
|
|
|
|
Sphere.prototype.getMinCorner = function () {
|
|
return this.center.add(this.temporaryTranslation).subtract(Vector.create([this.radius, this.radius, this.radius]));
|
|
};
|
|
|
|
Sphere.prototype.getMaxCorner = function () {
|
|
return this.center.add(this.temporaryTranslation).add(Vector.create([this.radius, this.radius, this.radius]));
|
|
};
|
|
|
|
Sphere.prototype.intersect = function (origin, ray) {
|
|
return Sphere.intersect(origin, ray, this.center.add(this.temporaryTranslation), this.radius);
|
|
};
|
|
|
|
Sphere.intersect = function (origin, ray, center, radius) {
|
|
var toSphere = origin.subtract(center);
|
|
var a = ray.dot(ray);
|
|
var b = 2 * toSphere.dot(ray);
|
|
var c = toSphere.dot(toSphere) - radius * radius;
|
|
var discriminant = b * b - 4 * a * c;
|
|
if (discriminant > 0) {
|
|
var t = (-b - Math.sqrt(discriminant)) / (2 * a);
|
|
if (t > 0) {
|
|
return t;
|
|
}
|
|
}
|
|
return Number.MAX_VALUE;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class Cube
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function Cube(minCorner, maxCorner, id) {
|
|
this.minCorner = minCorner;
|
|
this.maxCorner = maxCorner;
|
|
this.minStr = 'cubeMin' + id;
|
|
this.maxStr = 'cubeMax' + id;
|
|
this.intersectStr = 'tCube' + id;
|
|
this.temporaryTranslation = Vector.create([0, 0, 0]);
|
|
}
|
|
|
|
Cube.prototype.getGlobalCode = function () {
|
|
return '' +
|
|
' uniform vec3 ' + this.minStr + ';' +
|
|
' uniform vec3 ' + this.maxStr + ';';
|
|
};
|
|
|
|
Cube.prototype.getIntersectCode = function () {
|
|
return '' +
|
|
' vec2 ' + this.intersectStr + ' = intersectCube(origin, ray, ' + this.minStr + ', ' + this.maxStr + ');';
|
|
};
|
|
|
|
Cube.prototype.getShadowTestCode = function () {
|
|
return '' +
|
|
this.getIntersectCode() +
|
|
' if(' + this.intersectStr + '.x > 0.0 && ' + this.intersectStr + '.x < 1.0 && ' + this.intersectStr + '.x < ' + this.intersectStr + '.y) return 0.0;';
|
|
};
|
|
|
|
Cube.prototype.getMinimumIntersectCode = function () {
|
|
return '' +
|
|
' if(' + this.intersectStr + '.x > 0.0 && ' + this.intersectStr + '.x < ' + this.intersectStr + '.y && ' + this.intersectStr + '.x < t) t = ' + this.intersectStr + '.x;';
|
|
};
|
|
|
|
Cube.prototype.getNormalCalculationCode = function () {
|
|
return '' +
|
|
// have to compare intersectStr.x < intersectStr.y otherwise two coplanar
|
|
// cubes will look wrong (one cube will "steal" the hit from the other)
|
|
' else if(t == ' + this.intersectStr + '.x && ' + this.intersectStr + '.x < ' + this.intersectStr + '.y) normal = normalForCube(hit, ' + this.minStr + ', ' + this.maxStr + ');';
|
|
};
|
|
|
|
Cube.prototype.setUniforms = function (renderer) {
|
|
renderer.uniforms[this.minStr] = this.getMinCorner();
|
|
renderer.uniforms[this.maxStr] = this.getMaxCorner();
|
|
};
|
|
|
|
Cube.prototype.temporaryTranslate = function (translation) {
|
|
this.temporaryTranslation = translation;
|
|
};
|
|
|
|
Cube.prototype.translate = function (translation) {
|
|
this.minCorner = this.minCorner.add(translation);
|
|
this.maxCorner = this.maxCorner.add(translation);
|
|
};
|
|
|
|
Cube.prototype.getMinCorner = function () {
|
|
return this.minCorner.add(this.temporaryTranslation);
|
|
};
|
|
|
|
Cube.prototype.getMaxCorner = function () {
|
|
return this.maxCorner.add(this.temporaryTranslation);
|
|
};
|
|
|
|
Cube.prototype.intersect = function (origin, ray) {
|
|
return Cube.intersect(origin, ray, this.getMinCorner(), this.getMaxCorner());
|
|
};
|
|
|
|
Cube.intersect = function (origin, ray, cubeMin, cubeMax) {
|
|
var tMin = cubeMin.subtract(origin).componentDivide(ray);
|
|
var tMax = cubeMax.subtract(origin).componentDivide(ray);
|
|
var t1 = Vector.min(tMin, tMax);
|
|
var t2 = Vector.max(tMin, tMax);
|
|
var tNear = t1.maxComponent();
|
|
var tFar = t2.minComponent();
|
|
if (tNear > 0 && tNear < tFar) {
|
|
return tNear;
|
|
}
|
|
return Number.MAX_VALUE;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class Light
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function Light() {
|
|
this.temporaryTranslation = Vector.create([0, 0, 0]);
|
|
}
|
|
|
|
Light.prototype.getGlobalCode = function () {
|
|
return 'uniform vec3 light;';
|
|
};
|
|
|
|
Light.prototype.getIntersectCode = function () {
|
|
return '';
|
|
};
|
|
|
|
Light.prototype.getShadowTestCode = function () {
|
|
return '';
|
|
};
|
|
|
|
Light.prototype.getMinimumIntersectCode = function () {
|
|
return '';
|
|
};
|
|
|
|
Light.prototype.getNormalCalculationCode = function () {
|
|
return '';
|
|
};
|
|
|
|
Light.prototype.setUniforms = function (renderer) {
|
|
renderer.uniforms.light = light.add(this.temporaryTranslation);
|
|
};
|
|
|
|
Light.clampPosition = function (position) {
|
|
for (var i = 0; i < position.elements.length; i++) {
|
|
position.elements[i] = Math.max(lightSize - 1, Math.min(1 - lightSize, position.elements[i]));
|
|
}
|
|
};
|
|
|
|
Light.prototype.temporaryTranslate = function (translation) {
|
|
var tempLight = light.add(translation);
|
|
Light.clampPosition(tempLight);
|
|
this.temporaryTranslation = tempLight.subtract(light);
|
|
};
|
|
|
|
Light.prototype.translate = function (translation) {
|
|
light = light.add(translation);
|
|
Light.clampPosition(light);
|
|
};
|
|
|
|
Light.prototype.getMinCorner = function () {
|
|
return light.add(this.temporaryTranslation).subtract(Vector.create([lightSize, lightSize, lightSize]));
|
|
};
|
|
|
|
Light.prototype.getMaxCorner = function () {
|
|
return light.add(this.temporaryTranslation).add(Vector.create([lightSize, lightSize, lightSize]));
|
|
};
|
|
|
|
Light.prototype.intersect = function (origin, ray) {
|
|
return Number.MAX_VALUE;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class PathTracer
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function PathTracer() {
|
|
var vertices = [
|
|
-1, -1,
|
|
-1, +1,
|
|
+1, -1,
|
|
+1, +1
|
|
];
|
|
|
|
// create vertex buffer
|
|
this.vertexBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
|
|
|
// create framebuffer
|
|
this.framebuffer = gl.createFramebuffer();
|
|
|
|
// create textures
|
|
var type = gl.getExtension('OES_texture_float') ? gl.FLOAT : gl.UNSIGNED_BYTE;
|
|
this.textures = [];
|
|
for (var i = 0; i < 2; i++) {
|
|
this.textures.push(gl.createTexture());
|
|
gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 512, 512, 0, gl.RGB, type, null);
|
|
}
|
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
|
|
// create render shader
|
|
this.renderProgram = compileShader(renderVertexSource, renderFragmentSource);
|
|
this.renderVertexAttribute = gl.getAttribLocation(this.renderProgram, 'vertex');
|
|
gl.enableVertexAttribArray(this.renderVertexAttribute);
|
|
|
|
// objects and shader will be filled in when setObjects() is called
|
|
this.objects = [];
|
|
this.sampleCount = 0;
|
|
this.tracerProgram = null;
|
|
}
|
|
|
|
PathTracer.prototype.setObjects = function (objects) {
|
|
this.uniforms = {};
|
|
this.sampleCount = 0;
|
|
this.objects = objects;
|
|
|
|
// create tracer shader
|
|
if (this.tracerProgram != null) {
|
|
gl.deleteProgram(this.shaderProgram);
|
|
}
|
|
this.tracerProgram = compileShader(tracerVertexSource, makeTracerFragmentSource(objects));
|
|
this.tracerVertexAttribute = gl.getAttribLocation(this.tracerProgram, 'vertex');
|
|
gl.enableVertexAttribArray(this.tracerVertexAttribute);
|
|
};
|
|
|
|
PathTracer.prototype.update = function (matrix, timeSinceStart) {
|
|
// calculate uniforms
|
|
for (var i = 0; i < this.objects.length; i++) {
|
|
this.objects[i].setUniforms(this);
|
|
}
|
|
this.uniforms.eye = eye;
|
|
this.uniforms.glossiness = glossiness;
|
|
this.uniforms.ray00 = getEyeRay(matrix, -1, -1);
|
|
this.uniforms.ray01 = getEyeRay(matrix, -1, +1);
|
|
this.uniforms.ray10 = getEyeRay(matrix, +1, -1);
|
|
this.uniforms.ray11 = getEyeRay(matrix, +1, +1);
|
|
this.uniforms.timeSinceStart = timeSinceStart;
|
|
this.uniforms.textureWeight = this.sampleCount / (this.sampleCount + 1);
|
|
|
|
// set uniforms
|
|
gl.useProgram(this.tracerProgram);
|
|
setUniforms(this.tracerProgram, this.uniforms);
|
|
|
|
// render to texture
|
|
gl.useProgram(this.tracerProgram);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.textures[0]);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.textures[1], 0);
|
|
gl.vertexAttribPointer(this.tracerVertexAttribute, 2, gl.FLOAT, false, 0, 0);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
// ping pong textures
|
|
this.textures.reverse();
|
|
this.sampleCount++;
|
|
};
|
|
|
|
PathTracer.prototype.render = function () {
|
|
gl.useProgram(this.renderProgram);
|
|
gl.bindTexture(gl.TEXTURE_2D, this.textures[0]);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.vertexAttribPointer(this.renderVertexAttribute, 2, gl.FLOAT, false, 0, 0);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class Renderer
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function Renderer() {
|
|
var vertices = [
|
|
0, 0, 0,
|
|
1, 0, 0,
|
|
0, 1, 0,
|
|
1, 1, 0,
|
|
0, 0, 1,
|
|
1, 0, 1,
|
|
0, 1, 1,
|
|
1, 1, 1
|
|
];
|
|
var indices = [
|
|
0, 1, 1, 3, 3, 2, 2, 0,
|
|
4, 5, 5, 7, 7, 6, 6, 4,
|
|
0, 4, 1, 5, 2, 6, 3, 7
|
|
];
|
|
|
|
// create vertex buffer
|
|
this.vertexBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
|
|
|
// create index buffer
|
|
this.indexBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
|
|
|
// create line shader
|
|
this.lineProgram = compileShader(lineVertexSource, lineFragmentSource);
|
|
this.vertexAttribute = gl.getAttribLocation(this.lineProgram, 'vertex');
|
|
gl.enableVertexAttribArray(this.vertexAttribute);
|
|
|
|
this.objects = [];
|
|
this.selectedObject = null;
|
|
this.pathTracer = new PathTracer();
|
|
}
|
|
|
|
Renderer.prototype.setObjects = function (objects) {
|
|
this.objects = objects;
|
|
this.selectedObject = null;
|
|
this.pathTracer.setObjects(objects);
|
|
};
|
|
|
|
Renderer.prototype.update = function (modelviewProjection, timeSinceStart) {
|
|
var jitter = Matrix.Translation(Vector.create([Math.random() * 2 - 1, Math.random() * 2 - 1, 0]).multiply(1 / 512));
|
|
var inverse = jitter.multiply(modelviewProjection).inverse();
|
|
this.modelviewProjection = modelviewProjection;
|
|
this.pathTracer.update(inverse, timeSinceStart);
|
|
};
|
|
|
|
Renderer.prototype.render = function () {
|
|
this.pathTracer.render();
|
|
|
|
if (this.selectedObject != null) {
|
|
gl.useProgram(this.lineProgram);
|
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
gl.vertexAttribPointer(this.vertexAttribute, 3, gl.FLOAT, false, 0, 0);
|
|
setUniforms(this.lineProgram, {
|
|
cubeMin: this.selectedObject.getMinCorner(),
|
|
cubeMax: this.selectedObject.getMaxCorner(),
|
|
modelviewProjection: this.modelviewProjection
|
|
});
|
|
gl.drawElements(gl.LINES, 24, gl.UNSIGNED_SHORT, 0);
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// class UI
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function UI() {
|
|
this.renderer = new Renderer();
|
|
this.moving = false;
|
|
}
|
|
|
|
UI.prototype.setObjects = function (objects) {
|
|
this.objects = objects;
|
|
this.objects.splice(0, 0, new Light());
|
|
this.renderer.setObjects(this.objects);
|
|
};
|
|
|
|
UI.prototype.update = function (timeSinceStart) {
|
|
this.modelview = makeLookAt(eye.elements[0], eye.elements[1], eye.elements[2], 0, 0, 0, 0, 1, 0);
|
|
this.projection = makePerspective(55, 1, 0.1, 100);
|
|
this.modelviewProjection = this.projection.multiply(this.modelview);
|
|
this.renderer.update(this.modelviewProjection, timeSinceStart);
|
|
};
|
|
|
|
UI.prototype.mouseDown = function (x, y) {
|
|
var t;
|
|
var origin = eye;
|
|
var ray = getEyeRay(this.modelviewProjection.inverse(), (x / 512) * 2 - 1, 1 - (y / 512) * 2);
|
|
|
|
// test the selection box first
|
|
if (this.renderer.selectedObject != null) {
|
|
var minBounds = this.renderer.selectedObject.getMinCorner();
|
|
var maxBounds = this.renderer.selectedObject.getMaxCorner();
|
|
t = Cube.intersect(origin, ray, minBounds, maxBounds);
|
|
|
|
if (t < Number.MAX_VALUE) {
|
|
var hit = origin.add(ray.multiply(t));
|
|
|
|
if (Math.abs(hit.elements[0] - minBounds.elements[0]) < 0.001) this.movementNormal = Vector.create([-1, 0, 0]);
|
|
else if (Math.abs(hit.elements[0] - maxBounds.elements[0]) < 0.001) this.movementNormal = Vector.create([+1, 0, 0]);
|
|
else if (Math.abs(hit.elements[1] - minBounds.elements[1]) < 0.001) this.movementNormal = Vector.create([0, -1, 0]);
|
|
else if (Math.abs(hit.elements[1] - maxBounds.elements[1]) < 0.001) this.movementNormal = Vector.create([0, +1, 0]);
|
|
else if (Math.abs(hit.elements[2] - minBounds.elements[2]) < 0.001) this.movementNormal = Vector.create([0, 0, -1]);
|
|
else this.movementNormal = Vector.create([0, 0, +1]);
|
|
|
|
this.movementDistance = this.movementNormal.dot(hit);
|
|
this.originalHit = hit;
|
|
this.moving = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
t = Number.MAX_VALUE;
|
|
this.renderer.selectedObject = null;
|
|
|
|
for (var i = 0; i < this.objects.length; i++) {
|
|
var objectT = this.objects[i].intersect(origin, ray);
|
|
if (objectT < t) {
|
|
t = objectT;
|
|
this.renderer.selectedObject = this.objects[i];
|
|
}
|
|
}
|
|
|
|
return (t < Number.MAX_VALUE);
|
|
};
|
|
|
|
UI.prototype.mouseMove = function (x, y) {
|
|
if (this.moving) {
|
|
var origin = eye;
|
|
var ray = getEyeRay(this.modelviewProjection.inverse(), (x / 512) * 2 - 1, 1 - (y / 512) * 2);
|
|
|
|
var t = (this.movementDistance - this.movementNormal.dot(origin)) / this.movementNormal.dot(ray);
|
|
var hit = origin.add(ray.multiply(t));
|
|
this.renderer.selectedObject.temporaryTranslate(hit.subtract(this.originalHit));
|
|
|
|
// clear the sample buffer
|
|
this.renderer.pathTracer.sampleCount = 0;
|
|
}
|
|
};
|
|
|
|
UI.prototype.mouseUp = function (x, y) {
|
|
if (this.moving) {
|
|
var origin = eye;
|
|
var ray = getEyeRay(this.modelviewProjection.inverse(), (x / 512) * 2 - 1, 1 - (y / 512) * 2);
|
|
|
|
var t = (this.movementDistance - this.movementNormal.dot(origin)) / this.movementNormal.dot(ray);
|
|
var hit = origin.add(ray.multiply(t));
|
|
this.renderer.selectedObject.temporaryTranslate(Vector.create([0, 0, 0]));
|
|
this.renderer.selectedObject.translate(hit.subtract(this.originalHit));
|
|
this.moving = false;
|
|
}
|
|
};
|
|
|
|
UI.prototype.render = function () {
|
|
this.renderer.render();
|
|
};
|
|
|
|
UI.prototype.selectLight = function () {
|
|
this.renderer.selectedObject = this.objects[0];
|
|
};
|
|
|
|
UI.prototype.addSphere = function () {
|
|
this.objects.push(new Sphere(Vector.create([0, 0, 0]), 0.25, nextObjectId++));
|
|
this.renderer.setObjects(this.objects);
|
|
};
|
|
|
|
UI.prototype.addCube = function () {
|
|
this.objects.push(new Cube(Vector.create([-0.25, -0.25, -0.25]), Vector.create([0.25, 0.25, 0.25]), nextObjectId++));
|
|
this.renderer.setObjects(this.objects);
|
|
};
|
|
|
|
UI.prototype.deleteSelection = function () {
|
|
for (var i = 0; i < this.objects.length; i++) {
|
|
if (this.renderer.selectedObject == this.objects[i]) {
|
|
this.objects.splice(i, 1);
|
|
this.renderer.selectedObject = null;
|
|
this.renderer.setObjects(this.objects);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
UI.prototype.updateMaterial = function () {
|
|
var newMaterial = parseInt(document.getElementById('material').value, 10);
|
|
if (material != newMaterial) {
|
|
material = newMaterial;
|
|
this.renderer.setObjects(this.objects);
|
|
}
|
|
};
|
|
|
|
UI.prototype.updateEnvironment = function () {
|
|
var newEnvironment = parseInt(document.getElementById('environment').value, 10);
|
|
if (environment != newEnvironment) {
|
|
environment = newEnvironment;
|
|
this.renderer.setObjects(this.objects);
|
|
}
|
|
};
|
|
|
|
UI.prototype.updateGlossiness = function () {
|
|
var newGlossiness = parseFloat(document.getElementById('glossiness').value);
|
|
if (isNaN(newGlossiness)) newGlossiness = 0;
|
|
newGlossiness = Math.max(0, Math.min(1, newGlossiness));
|
|
if (material == MATERIAL_GLOSSY && glossiness != newGlossiness) {
|
|
this.renderer.pathTracer.sampleCount = 0;
|
|
}
|
|
glossiness = newGlossiness;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// main program
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var gl;
|
|
var ui;
|
|
var error;
|
|
var canvas;
|
|
var inputFocusCount = 0;
|
|
|
|
var angleX = 0;
|
|
var angleY = 0;
|
|
var zoomZ = 2.5;
|
|
var eye = Vector.create([0, 0, 0]);
|
|
var light = Vector.create([0.4, 0.5, -0.6]);
|
|
|
|
var nextObjectId = 0;
|
|
|
|
var MATERIAL_DIFFUSE = 0;
|
|
var MATERIAL_MIRROR = 1;
|
|
var MATERIAL_GLOSSY = 2;
|
|
var material = MATERIAL_DIFFUSE;
|
|
var glossiness = 0.6;
|
|
|
|
var YELLOW_BLUE_CORNELL_BOX = 0;
|
|
var RED_GREEN_CORNELL_BOX = 1;
|
|
var environment = YELLOW_BLUE_CORNELL_BOX;
|
|
|
|
function tick(timeSinceStart) {
|
|
eye.elements[0] = zoomZ * Math.sin(angleY) * Math.cos(angleX);
|
|
eye.elements[1] = zoomZ * Math.sin(angleX);
|
|
eye.elements[2] = zoomZ * Math.cos(angleY) * Math.cos(angleX);
|
|
|
|
ui.updateMaterial();
|
|
ui.updateGlossiness();
|
|
ui.updateEnvironment();
|
|
ui.update(timeSinceStart);
|
|
ui.render();
|
|
}
|
|
|
|
function makeStacks() {
|
|
var objects = [];
|
|
|
|
// lower level
|
|
objects.push(new Cube(Vector.create([-0.5, -0.75, -0.5]), Vector.create([0.5, -0.7, 0.5]), nextObjectId++));
|
|
|
|
// further poles
|
|
objects.push(new Cube(Vector.create([-0.45, -1, -0.45]), Vector.create([-0.4, -0.45, -0.4]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.4, -1, -0.45]), Vector.create([0.45, -0.45, -0.4]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([-0.45, -1, 0.4]), Vector.create([-0.4, -0.45, 0.45]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.4, -1, 0.4]), Vector.create([0.45, -0.45, 0.45]), nextObjectId++));
|
|
|
|
// upper level
|
|
objects.push(new Cube(Vector.create([-0.3, -0.5, -0.3]), Vector.create([0.3, -0.45, 0.3]), nextObjectId++));
|
|
|
|
// closer poles
|
|
objects.push(new Cube(Vector.create([-0.25, -0.7, -0.25]), Vector.create([-0.2, -0.25, -0.2]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.2, -0.7, -0.25]), Vector.create([0.25, -0.25, -0.2]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([-0.25, -0.7, 0.2]), Vector.create([-0.2, -0.25, 0.25]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.2, -0.7, 0.2]), Vector.create([0.25, -0.25, 0.25]), nextObjectId++));
|
|
|
|
// upper level
|
|
objects.push(new Cube(Vector.create([-0.25, -0.25, -0.25]), Vector.create([0.25, -0.2, 0.25]), nextObjectId++));
|
|
|
|
return objects;
|
|
}
|
|
|
|
function makeTableAndChair() {
|
|
var objects = [];
|
|
|
|
// table top
|
|
objects.push(new Cube(Vector.create([-0.5, -0.35, -0.5]), Vector.create([0.3, -0.3, 0.5]), nextObjectId++));
|
|
|
|
// table legs
|
|
objects.push(new Cube(Vector.create([-0.45, -1, -0.45]), Vector.create([-0.4, -0.35, -0.4]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.2, -1, -0.45]), Vector.create([0.25, -0.35, -0.4]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([-0.45, -1, 0.4]), Vector.create([-0.4, -0.35, 0.45]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.2, -1, 0.4]), Vector.create([0.25, -0.35, 0.45]), nextObjectId++));
|
|
|
|
// chair seat
|
|
objects.push(new Cube(Vector.create([0.3, -0.6, -0.2]), Vector.create([0.7, -0.55, 0.2]), nextObjectId++));
|
|
|
|
// chair legs
|
|
objects.push(new Cube(Vector.create([0.3, -1, -0.2]), Vector.create([0.35, -0.6, -0.15]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.3, -1, 0.15]), Vector.create([0.35, -0.6, 0.2]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.65, -1, -0.2]), Vector.create([0.7, 0.1, -0.15]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.65, -1, 0.15]), Vector.create([0.7, 0.1, 0.2]), nextObjectId++));
|
|
|
|
// chair back
|
|
objects.push(new Cube(Vector.create([0.65, 0.05, -0.15]), Vector.create([0.7, 0.1, 0.15]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.65, -0.55, -0.09]), Vector.create([0.7, 0.1, -0.03]), nextObjectId++));
|
|
objects.push(new Cube(Vector.create([0.65, -0.55, 0.03]), Vector.create([0.7, 0.1, 0.09]), nextObjectId++));
|
|
|
|
// sphere on table
|
|
objects.push(new Sphere(Vector.create([-0.1, -0.05, 0]), 0.25, nextObjectId++));
|
|
|
|
return objects;
|
|
}
|
|
|
|
function makeSphereAndCube() {
|
|
var objects = [];
|
|
objects.push(new Cube(Vector.create([-0.25, -1, -0.25]), Vector.create([0.25, -0.75, 0.25]), nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, -0.75, 0]), 0.25, nextObjectId++));
|
|
return objects;
|
|
}
|
|
|
|
function makeSphereColumn() {
|
|
var objects = [];
|
|
objects.push(new Sphere(Vector.create([0, 0.75, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, 0.25, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, -0.25, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, -0.75, 0]), 0.25, nextObjectId++));
|
|
return objects;
|
|
}
|
|
|
|
function makeCubeAndSpheres() {
|
|
var objects = [];
|
|
objects.push(new Cube(Vector.create([-0.25, -0.25, -0.25]), Vector.create([0.25, 0.25, 0.25]), nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([-0.25, 0, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([+0.25, 0, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, -0.25, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, +0.25, 0]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, 0, -0.25]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0, 0, +0.25]), 0.25, nextObjectId++));
|
|
return objects;
|
|
}
|
|
|
|
function makeSpherePyramid() {
|
|
var root3_over4 = 0.433012701892219;
|
|
var root3_over6 = 0.288675134594813;
|
|
var root6_over6 = 0.408248290463863;
|
|
var objects = [];
|
|
|
|
// first level
|
|
objects.push(new Sphere(Vector.create([-0.5, -0.75, -root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0.0, -0.75, -root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0.5, -0.75, -root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([-0.25, -0.75, root3_over4 - root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0.25, -0.75, root3_over4 - root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0.0, -0.75, 2.0 * root3_over4 - root3_over6]), 0.25, nextObjectId++));
|
|
|
|
// second level
|
|
objects.push(new Sphere(Vector.create([0.0, -0.75 + root6_over6, root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([-0.25, -0.75 + root6_over6, -0.5 * root3_over6]), 0.25, nextObjectId++));
|
|
objects.push(new Sphere(Vector.create([0.25, -0.75 + root6_over6, -0.5 * root3_over6]), 0.25, nextObjectId++));
|
|
|
|
// third level
|
|
objects.push(new Sphere(Vector.create([0.0, -0.75 + 2.0 * root6_over6, 0.0]), 0.25, nextObjectId++));
|
|
|
|
return objects;
|
|
}
|
|
|
|
var XNEG = 0, XPOS = 1, YNEG = 2, YPOS = 3, ZNEG = 4, ZPOS = 5;
|
|
|
|
function addRecursiveSpheresBranch(objects, center, radius, depth, dir) {
|
|
objects.push(new Sphere(center, radius, nextObjectId++));
|
|
if (depth--) {
|
|
if (dir != XNEG) addRecursiveSpheresBranch(objects, center.subtract(Vector.create([radius * 1.5, 0, 0])), radius / 2, depth, XPOS);
|
|
if (dir != XPOS) addRecursiveSpheresBranch(objects, center.add(Vector.create([radius * 1.5, 0, 0])), radius / 2, depth, XNEG);
|
|
|
|
if (dir != YNEG) addRecursiveSpheresBranch(objects, center.subtract(Vector.create([0, radius * 1.5, 0])), radius / 2, depth, YPOS);
|
|
if (dir != YPOS) addRecursiveSpheresBranch(objects, center.add(Vector.create([0, radius * 1.5, 0])), radius / 2, depth, YNEG);
|
|
|
|
if (dir != ZNEG) addRecursiveSpheresBranch(objects, center.subtract(Vector.create([0, 0, radius * 1.5])), radius / 2, depth, ZPOS);
|
|
if (dir != ZPOS) addRecursiveSpheresBranch(objects, center.add(Vector.create([0, 0, radius * 1.5])), radius / 2, depth, ZNEG);
|
|
}
|
|
}
|
|
|
|
function makeRecursiveSpheres() {
|
|
var objects = [];
|
|
addRecursiveSpheresBranch(objects, Vector.create([0, 0, 0]), 0.3, 2, -1);
|
|
return objects;
|
|
}
|
|
|
|
window.onload = function () {
|
|
gl = null;
|
|
canvas = document.getElementById('canvas');
|
|
try { gl = canvas.getContext('experimental-webgl'); } catch (e) { }
|
|
|
|
if (gl) {
|
|
// keep track of whether an <input> is focused or not (will be no only if inputFocusCount == 0)
|
|
var inputs = document.getElementsByTagName('input');
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
inputs[i].onfocus = function () { inputFocusCount++; };
|
|
inputs[i].onblur = function () { inputFocusCount--; };
|
|
}
|
|
|
|
material = parseInt(document.getElementById('material').value, 10);
|
|
environment = parseInt(document.getElementById('environment').value, 10);
|
|
ui = new UI();
|
|
ui.setObjects(makeSpherePyramid());
|
|
var start = new Date();
|
|
setInterval(function () { tick((new Date() - start) * 0.001); }, 1000 / 60);
|
|
}
|
|
};
|
|
|
|
function elementPos(element) {
|
|
var x = 0, y = 0;
|
|
while (element.offsetParent) {
|
|
x += element.offsetLeft;
|
|
y += element.offsetTop;
|
|
element = element.offsetParent;
|
|
}
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
function eventPos(event) {
|
|
return {
|
|
x: event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft,
|
|
y: event.clientY + document.body.scrollTop + document.documentElement.scrollTop
|
|
};
|
|
}
|
|
|
|
function canvasMousePos(event) {
|
|
var mousePos = eventPos(event);
|
|
var canvasPos = elementPos(canvas);
|
|
return {
|
|
x: mousePos.x - canvasPos.x,
|
|
y: mousePos.y - canvasPos.y
|
|
};
|
|
}
|
|
|
|
var mouseDown = false, oldX, oldY;
|
|
|
|
document.onmousedown = function (event) {
|
|
var mouse = canvasMousePos(event);
|
|
oldX = mouse.x;
|
|
oldY = mouse.y;
|
|
|
|
if (mouse.x >= 0 && mouse.x < 512 && mouse.y >= 0 && mouse.y < 512) {
|
|
mouseDown = !ui.mouseDown(mouse.x, mouse.y);
|
|
|
|
// disable selection because dragging is used for rotating the camera and moving objects
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
document.onmousemove = function (event) {
|
|
var mouse = canvasMousePos(event);
|
|
|
|
if (mouseDown) {
|
|
// update the angles based on how far we moved since last time
|
|
angleY -= (mouse.x - oldX) * 0.01;
|
|
angleX += (mouse.y - oldY) * 0.01;
|
|
|
|
// don't go upside down
|
|
angleX = Math.max(angleX, -Math.PI / 2 + 0.01);
|
|
angleX = Math.min(angleX, Math.PI / 2 - 0.01);
|
|
|
|
// clear the sample buffer
|
|
ui.renderer.pathTracer.sampleCount = 0;
|
|
|
|
// remember this coordinate
|
|
oldX = mouse.x;
|
|
oldY = mouse.y;
|
|
} else {
|
|
var canvasPos = elementPos(canvas);
|
|
ui.mouseMove(mouse.x, mouse.y);
|
|
}
|
|
};
|
|
|
|
document.onmouseup = function (event) {
|
|
mouseDown = false;
|
|
|
|
var mouse = canvasMousePos(event);
|
|
ui.mouseUp(mouse.x, mouse.y);
|
|
};
|
|
|
|
document.onkeydown = function (event) {
|
|
// if there are no <input> elements focused
|
|
if (inputFocusCount == 0) {
|
|
// if backspace or delete was pressed
|
|
if (event.keyCode == 8 || event.keyCode == 46) {
|
|
ui.deleteSelection();
|
|
|
|
// don't let the backspace key go back a page
|
|
return false;
|
|
}
|
|
}
|
|
};
|