Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Cleaned up materials<->lighting code, intersection routines now retur…

…n normals, box added to scene.
  • Loading branch information...
commit d2ccebac54db5ae9f0a3a4056605bddd703f6cf3 1 parent f34a200
@chriskillpack authored
View
8 index.html
@@ -37,9 +37,9 @@
</td>
<td>
<div style="width:30em">
- The raytracer is written entirely in JavaScript. It uses an HTML5 <i>canvas</i> element to display the image, which is managed by <a href="http://processingjs.org">Processing.js</a>.
+ The raytracer is written entirely in JavaScript. It uses an HTML5 <i>canvas</i> element to display the image, which is managed by <a href="http://processingjs.org">Processing.js</a>. In time I will be removing the Processing code.
<br><br>
- The raytracer currently supports plane and sphere primitives, a material system, multiple lights and multi-sampling in image space. In time interactive features will be added.
+ The raytracer currently supports plane, sphere and box primitives, a material system, multiple lights and multi-sampling in image space. Over time interactive features will be added.
</div>
</td>
</tr>
@@ -47,8 +47,8 @@
<div>
<p>To me the most interesting part of this raytracer is not that it is written in JavaScript. I wrote it in JavaScript because it's my preferred language of the moment -- it's very quick to iterate in, and distribution is trivial. The quick speed of iteration allows me to play with design ideas, none of which are groundbreaking, but are just things I'm trying to do cleanly. I'm still working out details, but once they are nailed down I'll discuss them below.
-<p>A quick preview: image sampling implemented as a Strategy via an Iterator-style API; a <a href="https://renderman.pixar.com/products/whats_renderman/2.html">Renderman</a>-style <i>illuminate()</i> loop; and, composite Materials. I'd also like to experiment with HTML5's <a href="http://www.whatwg.org/specs/web-workers/current-work/">Web Workers</a>, and maybe turn this into a JavaScript benchmark of some sort.
-<p>Once the code is a little bit cleaner I'll be releasing the source. License and repository TBD.
+<p>A quick preview: image sampling implemented as a Strategy via an Iterator-style API; a <a href="https://renderman.pixar.com/products/whats_renderman/2.html">Renderman</a>-style <i>illuminate()</i> loop; and composite Materials. I'd also like to experiment with HTML5's <a href="http://www.whatwg.org/specs/web-workers/current-work/">Web Workers</a>, and maybe turn this into a JavaScript benchmark of some sort.
+<p><b>Code</b>: I've released the <a href="http://github.com/chriskillpack/raytracer">source code</a> on GitHub under the MIT license.
</ul>
</div>
</body>
View
64 lights.js
@@ -42,18 +42,52 @@ Lights.prototype.addLight = function(light) {
/**
- * @param {function(this:Material, light)} closure Callback from the material
- * shader that will be executed per light.
+ * Iterate over each light in the scene evaluating it's contribution to the
+ * given point and executing the provided material context.
+ * @param {Vector3} pos The world position of the point receiving the
+ * light.
+ * @param {Vector3} normal The world normal of the point receiving the light.
+ * @param {function(this:Material, irradiance)} closure Material shader
+ * callback executed for each light.
* @param {Material} context The context the closure will run in.
*/
-Lights.prototype.forEachLight = function(closure, context) {
+Lights.prototype.forEachLight = function(pos, normal, closure, context) {
for (var i = 0; i < this.lights_.length; i++) {
- closure.call(context, this.lights_[i]);
+ var irradiance = this.lights_[i].evaluateLight(pos, normal);
+ closure.call(context, irradiance);
}
};
/**
+ * A POD structure that holds the result of a lighting calculation.
+ * @param {Vector3} direction The direction of the irradiance on the point.
+ * @param {Vector3} color The color and intensity of the irradiance.
+ * @param {boolean} lightVisible Whether the light can see the point or not.
+ * @constructor
+ */
+function Irradiance(direction, color, lightVisible) {
+ /**
+ * The direction of the irradiance on the point.
+ * @type {Vector3}
+ */
+ this.direction = direction;
+
+ /**
+ * The color and intensity of the irradiance.
+ * @type {Vector3}
+ */
+ this.color = color;
+
+ /**
+ * Whether the light can see the point.
+ * @type {boolean}
+ */
+ this.lightVisible = lightVisible;
+}
+
+
+/**
* Implements a directional light.
* @param {Vector3} direction The normalized direction of the light.
* @param {Vector3} color The color of the light.
@@ -91,10 +125,7 @@ DirectionalLight.LIGHT_STEP_SIZE_ = 10000;
* @param {Vector3} pos The world position of the surface point.
* @param {Vector3} normal The lighting normal in world space for the surface
* point.
- * @return {{lightVisible:boolean, direction:Vector3, color:Vector3}}
- * lightVisible is true if the point can see the light, direction is the
- * incoming direction of the light on the point and color holds the color
- * (and intensity) of the light arriving at the point.
+ * @return {Irradiance} The result of the light evaluation for the input point.
*/
DirectionalLight.prototype.evaluateLight = function(pos, normal) {
// Shadow test - can the surface point 'see' the light?
@@ -105,15 +136,10 @@ DirectionalLight.prototype.evaluateLight = function(pos, normal) {
var lightPosition = Vector3.addMul(pos, this.direction_,
-DirectionalLight.LIGHT_STEP_SIZE_);
var ray = new Ray(lightPosition, this.direction_);
- var shadowTest = intersectRayWithScene(ray, true);
- var lightVisible = (shadowTest === undefined ||
- Math.abs(shadowTest.t -
- DirectionalLight.LIGHT_STEP_SIZE_) < 1e-2);
- // Build the response.
- var r = {};
- r.lightVisible = lightVisible;
- r.direction = this.direction_;
- r.color = this.color_;
-
- return r;
+ var shadowTest = intersectRayWithScene(ray);
+ // TODO: This test will be unnecessary once we move to testing ray segments.
+ var lightVisible = Math.abs(shadowTest.t -
+ DirectionalLight.LIGHT_STEP_SIZE_) < 1e-2;
+
+ return new Irradiance(this.direction_, this.color_, lightVisible);
};
View
80 materials.js
@@ -44,10 +44,10 @@ Material.prototype.evaluate = function(context) {
/**
* Compute the radiance of the material to the incoming light.
- * @param {{lightVisible:boolean, direction:Vector3, color:Vector3}}
- * irradiance The light reaching the point on the material.
- * @param {Vector3} pos The world position of the shaded point.
- * @param {Vector3} normal The geometric normal of the shaded point.
+ * @param {Irradiance} irradiance The light reaching the point on the material.
+ * @param {Vector3} pos The world space position of the point to be shaded.
+ * @param {Vector3} normal The geometric normal of the shaded point in world
+ * space.
* @param {ShadeContext} context The shade context.
* @return {Vector3} Material reflectance.
* @private
@@ -85,11 +85,17 @@ AmbientMaterial.prototype.evaluate = function(context) {
/**
- * Compute the reflectance of the material to the incoming light.
+ * Compute the radiance of the AmbientMaterial to the incoming light.
+ * @param {Irradiance} irradiance The light reaching the point on the material.
+ * @param {Vector3} pos The world space position of the point to be shaded.
+ * @param {Vector3} normal The geometric normal of the shaded point in world
+ * space.
+ * @param {ShadeContext} context The shade context.
* @return {Vector3} Material reflectance.
* @private
*/
-AmbientMaterial.prototype.evaluateRadiance_ = function() {
+AmbientMaterial.prototype.evaluateRadiance_ = function(irradiance, pos, normal,
+ context) {
return this.color_;
};
@@ -125,10 +131,7 @@ DiffuseMaterial.prototype.evaluate = function(context) {
var worldPos = context.ray.pointOnRay(context.t);
var color = new Vector3(0, 0, 0);
- g_lights.forEachLight(function(light) {
- // Evaluate the light with respect to this surface point.
- var incidentLight = light.evaluateLight(worldPos, context.normal);
-
+ g_lights.forEachLight(worldPos, context.normal, function(incidentLight) {
// Compute the material's response to the incoming light.
var c = this.evaluateRadiance_(incidentLight, worldPos, context.normal);
color.add(c);
@@ -142,10 +145,9 @@ DiffuseMaterial.prototype.evaluate = function(context) {
/**
* Compute the reflectance of the material to the incoming light.
- * @param {{lightVisible:boolean, direction:Vector3, color:Vector3}}
- * irradiance The light reaching the point on the material.
- * @param {Vector3} pos The world position of the shaded point.
- * @param {Vector3} normal The geometric normal of the shaded point.
+ * @param {Irradiance} irradiance The light reaching the point on the material.
+ * @param {Vector3} pos The world space position of the point to be shaded.
+ * @param {Vector3} normal The geometric normal of the point to be shaded.
* @param {ShadeContext} context The shade context.
* @return {Vector3} Material reflectance.
* @private
@@ -203,10 +205,7 @@ SpecularMaterial.prototype.evaluate = function(context) {
var diffuseColor = new Vector3(0, 0, 0);
var specularColor = new Vector3(0, 0, 0);
- g_lights.forEachLight(function(light) {
- // Evaluate the light with respect to this surface point.
- var incidentLight = light.evaluateLight(worldPos, context.normal);
-
+ g_lights.forEachLight(worldPos, context.normal, function(incidentLight) {
// Compute the diffuse material's reaction to the incident light.
diffuseColor.add(
this.diffuseMaterial_.evaluateRadiance_(
@@ -224,8 +223,7 @@ SpecularMaterial.prototype.evaluate = function(context) {
/**
* Compute the reflectance of the material to the incoming light.
- * @param {{lightVisible:boolean, direction:Vector3, color:Vector3}}
- * irradiance The light reaching the point on the material.
+ * @param {Irradiance} irradiance The light reaching the point on the material.
* @param {Vector3} pos The world position of the shaded point.
* @param {Vector3} normal The geometric normal of the shaded point.
* @param {ShadeContext} context The shade context.
@@ -254,42 +252,6 @@ function CheckerMaterial(material1, material2, size) {
this.size = size;
}
-
-/**
- * Returns the fractional part of a number.
- * @param {number} x The input value.
- * @return {number} The fractional part of x.
- * @private
- */
-// TODO: Convert to constructor local function.
-CheckerMaterial.frac_ = function(x) {
- return x - Math.floor(x);
-};
-
-
-/**
- * Returns whether a number is even.
- * @param {number} x The input value.
- * @return {boolean} True if the number is even.
- * @private
- */
-CheckerMaterial.even_ = function(x) {
- return CheckerMaterial.frac_(x / 2) < 1e-1;
-};
-
-
-/**
- * Perform a logical XOR of the two inputs.
- * @param {boolean} a One input.
- * @param {boolean} b The other input.
- * @return {boolean} The logical XOR of the two inputs.
- * @private
- */
-CheckerMaterial.xor_ = function(a, b) {
- return (a && !b) || (!a && b);
-};
-
-
/**
* Evaluate the material with the shade context.
* @param {ShadeContext} context The shade context.
@@ -298,8 +260,8 @@ CheckerMaterial.xor_ = function(a, b) {
CheckerMaterial.prototype.evaluate = function(context) {
// Compute the world position
var worldPos = context.ray.pointOnRay(context.t);
- var x = CheckerMaterial.even_(Math.floor(worldPos.x / 4));
- var z = CheckerMaterial.even_(Math.floor(worldPos.z / 4));
- var material = CheckerMaterial.xor_(x, z) ? this.material1 : this.material2;
+ var x = Math.floor(worldPos.x / 4) & 1;
+ var z = Math.floor(worldPos.z / 4) & 1;
+ var material = (x ^ z) ? this.material1 : this.material2;
return material.evaluate(context);
};
View
206 objects.js
@@ -19,6 +19,45 @@
// THE SOFTWARE.
/**
+ * The base class for all scene objects.
+ * @param {Material} material The object's material.
+ * @constructor
+ */
+function Object(material) {
+ /**
+ * @type {Material}
+ * @private
+ */
+ this.material_ = material;
+}
+
+
+/**
+ * Invoke the object's material to shade the intersection point.
+ * @param {RayContext} context The constructed context of the ray
+ * intersection.
+ * @return {Vector3} The color output of the shade operation.
+ */
+Object.prototype.shade = function(context) {
+ return this.material_.evaluate(context);
+};
+
+
+/**
+ * Perform a ray-object intersection test. This function must be implemented
+ * by all classes that inherit from Object.
+ * @param {Ray} ray The ray to be tested.
+ * @return {{t: number, normal: Vector3}} t holds the parametric distance along
+ * the ray to the closest point of intersection with the object, normal
+ * holds the object normal at the point of intersection. If there is no
+ * intersection then undefined is returned.
+ */
+Object.prototype.intersect = function(ray) {
+ throw 'intersect is not implemented.';
+};
+
+
+/**
* Implement a Sphere primitive.
* @param {Vector3} center The center of the sphere.
* @param {number} radius The radius of the sphere.
@@ -26,6 +65,8 @@
* @constructor
*/
function Sphere(center, radius, material) {
+ Object.call(this, material);
+
/**
* @type {Vector3}
* @private
@@ -37,20 +78,16 @@ function Sphere(center, radius, material) {
* @private
*/
this.radius_ = radius;
-
- /**
- * @type {Material}
- * @private
- */
- this.material_ = material;
}
-
+Sphere.prototype = new Object();
/**
* Perform a ray-sphere intersection test.
* @param {Ray} ray The ray to be tested.
- * @return {Array.<number>} The array of the ray's parametrics values at
- * points of intersection with the sphere.
+ * @return {{t: number, normal: Vector3}} t holds the parametric distance along
+ * the ray to the closest point of intersection with the sphere, normal
+ * holds the sphere normal at the point of intersection. If there is no
+ * intersection then undefined is returned.
*/
Sphere.prototype.intersect = function(ray) {
/**
@@ -64,21 +101,23 @@ Sphere.prototype.intersect = function(ray) {
var discrim_sq = b * b - 4 * a * c;
if (discrim_sq < 0) {
- return [];
+ return undefined;
}
- var intersections = new Array();
var discrim = Math.sqrt(discrim_sq);
if (Math.abs(discrim_sq) > 1e-2) {
- // Two intersections
- intersections.push((-b - discrim) / (2 * a));
- intersections.push((-b + discrim) / (2 * a));
+ // Two intersections, return the closer one. For reference the other is at
+ // (-b + discrim) / (2 * a).
+ var t = (-b - discrim) / (2 * a);
} else {
- // Glancing, one solution
- intersections.push(-b / (2 * a));
+ // Glancing intersection, with one solution.
+ var t = -b / (2 * a);
}
- return intersections;
+ var r = {};
+ r.t = t;
+ r.normal = this.normal(ray, t);
+ return r;
};
@@ -97,18 +136,6 @@ Sphere.prototype.normal = function(ray, t) {
/**
- * Invoke the object's material to shade the intersection point.
- * @param {RayContext} context The constructed context of the ray
- * intersection.
- * @return {Vector3} The color output of the shade operation.
- */
-Sphere.prototype.shade = function(context) {
- context.normal = this.normal(context.ray, context.t);
- return this.material_.evaluate(context);
-};
-
-
-/**
* Implements a plane primitive.
* @param {Vector3} normal The plane normal.
* @param {number} offset The distance of the plane along the normal from
@@ -117,6 +144,8 @@ Sphere.prototype.shade = function(context) {
* @constructor
*/
function Plane(normal, offset, material) {
+ Object.call(this, material);
+
/**
* @type {Vector3} normal
* @private
@@ -128,20 +157,17 @@ function Plane(normal, offset, material) {
* @private
*/
this.offset_ = offset;
-
- /**
- * @type {Material}
- * @private
- */
- this.material_ = material;
}
+Plane.prototype = new Object();
/**
* Test for an intersection between the ray and the plane.
* @param {Ray} ray The ray to intersect with the plane.
- * @return {Array.<number>} The array of the ray's parametrics values at
- * points of intersection with the plane.
+ * @return {{t: number, normal: Vector3}} t holds the parametric distance along
+ * the ray to the closest point of intersection with the plane, normal
+ * holds the plane normal at the point of intersection. If there is no
+ * intersection then undefined is returned.
*/
Plane.prototype.intersect = function(ray) {
/**
@@ -150,28 +176,19 @@ Plane.prototype.intersect = function(ray) {
var Vd = Vector3.dot(this.normal_, ray.direction);
if (Math.abs(Vd) < 1e-2) {
// Parallel to the plane, no intersection
- return [];
+ return undefined;
}
var V0 = -(Vector3.dot(this.normal_, ray.origin) - this.offset_);
var t = V0 / Vd;
if (t < 0) {
- // Intersection is behind eye origin, ignore
- return [];
+ // Intersection is behind ray origin, ignore.
+ return undefined;
}
- return [t];
-};
-
-
-/**
- * Invoke the object's material to shade the intersection point.
- * @param {RayContext} context The constructed context of the ray
- * intersection.
- * @return {Vector3} The color output of the shade operation.
- */
-Plane.prototype.shade = function(context) {
- context.normal = this.normal_;
- return this.material_.evaluate(context);
+ var r = {};
+ r.t = t;
+ r.normal = this.normal_;
+ return r;
};
@@ -184,18 +201,13 @@ Plane.prototype.shade = function(context) {
* @constructor
*/
function Box(width, height, depth, center, material) {
+ Object.call(this, material);
+
var width2 = width / 2;
var height2 = height / 2;
var depth2 = depth / 2;
/**
- * The material used for shading.
- * @type {Material}
- * @private
- */
- this.material_ = material;
-
- /**
* The 'minimal' corner of the AABB.
* @type {Vector3}
* @private
@@ -209,6 +221,7 @@ function Box(width, height, depth, center, material) {
*/
this.p1_ = Vector3.add(center, new Vector3(width2, height2, depth2));
}
+Box.prototype = new Object();
/**
@@ -243,53 +256,71 @@ Box.prototype.intersect = function(ray) {
}
// Find the biggest of txMin, tyMin and tzMin.
- var t0 = Math.max(txMin, Math.max(tyMin, tzMin));
+ // Also tracks the normal of the intersecting face.
+ var t0 = txMin;
+ var normal = new Vector3(-Box.sign_(ray.direction.x), 0, 0);
+ if (t0 < tyMin) {
+ t0 = tyMin;
+ normal = new Vector3(0, -Box.sign_(ray.direction.y), 0);
+ }
+ if (t0 < tzMin) {
+ t0 = tzMin;
+ normal = new Vector3(0, 0, -Box.sign_(ray.direction.z));
+ }
+
// Find the smallest of txMax, tyMax and tzMax.
var t1 = Math.min(txMax, Math.min(tyMax, tzMax));
if (t0 < t1) {
- // Intersection.
- return [t0, t1];
+ // Intersection. The two points of intersection are [t0, t1], but only
+ // the closer point is returned.
+ var r = {};
+ r.t = t0;
+ r.normal = normal;
+ return r;
}
// No intersection.
- return [];
+ return undefined;
};
/**
- * Invoke the object's material to shade the intersection point.
- * @param {RayContext} context The constructed context of the ray
- * intersection.
- * @return {Vector3} The color output of the shade operation.
+ * Return the sign of a number.
+ * @param {number} x The number to be tested.
+ * @return {number} -1 if the input is negative, 1 if it is positive, 0
+ * otherwise.
+ * @private
*/
-Box.prototype.shade = function(context) {
- // HACK!
- context.normal = new Vector3(0, 1, 0);
- return this.material_.evaluate(context);
+Box.sign_ = function(x) {
+ if (Math.abs(x) < 1e-5) {
+ return 0;
+ }
+
+ return (x < 0) ? -1 : 1;
};
+/**
+ * Test all objects in the scene for intersection with the ray.
+ * @param {Ray} ray The ray to intersect with the scene.
+ * @return {{t: number, normal: Vector3, obj: Object}} The closest intersection
+ * along the ray, the normal at the point of intersection and the object
+ * that intersected the ray, or undefined if the ray does not intersect
+ * any objects.
+ */
// TODO: Make this a method of an scene container.
// TODO: This function should test ray segments against objects for
// intersection.
-function intersectRayWithScene(ray, opt_stopOnFirstIntersection,
- opt_skipObject) {
+function intersectRayWithScene(ray) {
var closest_t = Infinity;
var closest_obj = undefined;
+ var closest_intersection = undefined;
for (var objectIdx = 0; objectIdx < g_objects.length; objectIdx++) {
- if (g_objects[objectIdx] == opt_skipObject) {
- continue;
- }
-
- var t = g_objects[objectIdx].intersect(ray);
- for (var items = 0; items < t.length; items++) {
- if (t[items] < closest_t) {
- closest_t = t[items];
- closest_obj = g_objects[objectIdx];
- }
- }
- if (closest_obj && opt_stopOnFirstIntersection) {
- break;
+ var intersect = g_objects[objectIdx].intersect(ray);
+ if (intersect && intersect.t < closest_t) {
+ closest_t = intersect.t;
+ closest_obj = g_objects[objectIdx];
+ closest_intersection = intersect;
}
}
@@ -301,6 +332,7 @@ function intersectRayWithScene(ray, opt_stopOnFirstIntersection,
// statements. So we break it apart for now.
var r = {};
r.t = closest_t;
+ r.normal = closest_intersection.normal;
r.obj = closest_obj;
return r;
}
View
12 raytracer.js
@@ -35,13 +35,14 @@ function initScene() {
new Vector3(0.4, 0.4, 0));
var blueCheck = new DiffuseMaterial(new Vector3(0, 0, 1),
new Vector3(0, 0, 0.4));
- var plane = new Plane(new Vector3(0, 1, 0), -2,
+ var plane = new Plane(new Vector3(0, 1, 0), -4,
new CheckerMaterial(yellowCheck, blueCheck));
g_objects.push(plane);
- //var box = new Box(7, 3, 3, new Vector3(1,1,1),
- // new AmbientMaterial(new Vector3(0,1,0)));
- //g_objects.push(box);
+ var box = new Box(2, 2, 2, new Vector3(-3.5, -2.5, 1),
+ new DiffuseMaterial(new Vector3(0, 1, 0),
+ new Vector3(0, 0.2, 0)));
+ g_objects.push(box);
g_sampler = new Sampler(1);
@@ -112,7 +113,8 @@ void draw() {
var shadeContext = {
object: intersection.obj,
ray: ray,
- t: intersection.t
+ t: intersection.t,
+ normal: intersection.normal
};
var color = intersection.obj.shade(shadeContext);
sampler.accumulateSample(color);
View
37 sampler.js
@@ -36,10 +36,23 @@
* @constructor
*/
function Sampler(numSamples) {
- this.numSamples = numSamples;
-
- this.remainingSamples = numSamples;
- this.colorAccumulator = new Vector3(0, 0, 0);
+ /**
+ * @type {number}
+ * @private
+ */
+ this.numSamples_ = numSamples;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.remainingSamples_ = numSamples;
+
+ /**
+ * @type {Vector3}
+ * @private
+ */
+ this.colorAccumulator_ = new Vector3(0, 0, 0);
}
@@ -48,7 +61,7 @@ function Sampler(numSamples) {
* @return {boolean} True if there is another sample point, False otherwise.
*/
Sampler.prototype.hasNext = function() {
- return this.remainingSamples > 0;
+ return this.remainingSamples_ > 0;
};
@@ -58,15 +71,15 @@ Sampler.prototype.hasNext = function() {
* @return {Vector2} The sample point.
*/
Sampler.prototype.next = function() {
- if (this.remainingSamples <= 0) {
+ if (this.remainingSamples_ <= 0) {
return undefined;
}
- this.remainingSamples = this.remainingSamples - 1;
+ this.remainingSamples_ = this.remainingSamples_ - 1;
// Setup state for accumulateSample().
// For now we use a box filter.
- this.sampleWeight = 1 / this.numSamples;
+ this.sampleWeight = 1 / this.numSamples_;
return new Vector2(0, 0);
};
@@ -77,8 +90,8 @@ Sampler.prototype.next = function() {
*/
Sampler.prototype.reset = function() {
// Reset for the next pixel.
- this.remainingSamples = this.numSamples;
- this.colorAccumulator.set(0, 0, 0);
+ this.remainingSamples_ = this.numSamples_;
+ this.colorAccumulator_.set(0, 0, 0);
};
@@ -88,7 +101,7 @@ Sampler.prototype.reset = function() {
* @param {Vector3} color The color for this sample.
*/
Sampler.prototype.accumulateSample = function(color) {
- this.colorAccumulator.addMul(color, this.sampleWeight);
+ this.colorAccumulator_.addMul(color, this.sampleWeight);
};
@@ -97,5 +110,5 @@ Sampler.prototype.accumulateSample = function(color) {
* @return {Vector3} Pixel color.
*/
Sampler.prototype.result = function() {
- return this.colorAccumulator;
+ return this.colorAccumulator_;
};
Please sign in to comment.
Something went wrong with that request. Please try again.