Skip to content

Instantly share code, notes, and snippets.

@pingbird
Created June 16, 2019 07:21
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pingbird/84377d383c20d056664e80849c5b79e9 to your computer and use it in GitHub Desktop.
Save pingbird/84377d383c20d056664e80849c5b79e9 to your computer and use it in GitHub Desktop.
Web raytracer in Dart
import 'dart:html';
import 'dart:math';
import 'dart:typed_data';
import 'package:vector_math/vector_math.dart';
// Base class for all objects in a raytraced scene
abstract class RTObject {
// Get the delta for a ray intersection, null otherwise
double intersect(Ray ray);
// Calculate final observable color of the hit object
Vector3 fill(RTScene scene, Ray ray, double dt, int depth);
// The following two vectors are used for light sources
Vector3 center;
Vector3 emissionColor = Vector3.zero();
}
const maxDepth = 3; // Maximum number of times a ray can bounce
var bounceOffset = 1e-5; // Offset to prevent a ray from hitting the surface it just came from
abstract class BasicRTObject extends RTObject {
Vector3 get surfaceColor;
Vector3 get emissionColor;
double get transparency;
double get reflection;
// Calculate the normal of a surface at a given ray hit position
Vector3 normal(Ray ray, double dt, Vector3 hitPos);
Vector3 fill(RTScene scene, Ray ray, double dt, int depth) {
var hitPos = ray.origin + ray.direction * dt;
var hitNormal = normal(ray, dt, hitPos);
// Fake light from bottom
var bl = max(0, hitNormal.dot(Vector3(0, -1, 0)) * 0.3);
var outColor = Vector3(bl, bl, bl);
if (surfaceColor != Vector3.zero()) outColor += scene.traceLight(surfaceColor, hitPos, hitNormal);
if ((transparency > 0 || reflection > 0) && depth < maxDepth) {
// Attempt at calculating the surface color of a PBR material
var facingRatio = -ray.direction.dot(hitNormal);
var fresnelEffect = pow(1 - facingRatio, 3);
var refldir = (ray.direction - hitNormal * 2 * ray.direction.dot(hitNormal))..normalize();
var nray = Ray.originDirection(hitPos + hitNormal * bounceOffset, refldir);
var reflection = scene.traceFill(nray, depth + 1);
outColor = (
reflection * max(fresnelEffect * this.reflection, this.reflection)
) + (outColor * (1 - this.reflection));
}
return outColor + emissionColor;
}
}
class SphereObject extends BasicRTObject {
SphereObject(this.center, this.radius, {
this.surfaceColor,
this.emissionColor,
this.transparency = 0.0,
this.reflection = 0.0,
}) : radiusSqr = radius * radius {
surfaceColor ??= Vector3.zero();
emissionColor ??= Vector3.zero();
}
Vector3 surfaceColor;
Vector3 emissionColor;
double transparency;
double reflection;
Vector3 center;
double radius;
double radiusSqr;
intersect(Ray ray) {
// Fast sphere intersection algorithm
var pt = center - ray.origin;
var dir = pt.dot(ray.direction);
if (dir < 0) return null;
var d2 = pt.dot(pt) - (dir * dir);
if (d2 > radiusSqr) return null;
var thc = sqrt(radiusSqr - d2);
return dir > thc ? dir - thc : dir + thc;
}
normal(Ray ray, double dt, Vector3 hitPos) =>
(hitPos - center)..normalize();
}
class RTScene {
List<RTObject> objects = [];
// Get the fill color for a given ray
Vector3 traceFill(Ray ray, int depth) {
RTObject hit;
double tnear = double.infinity;
for (var o in objects) {
double near;
if ((near = o.intersect(ray)) != null) {
if (near < tnear) {
tnear = near;
hit = o;
}
}
}
if (hit == null) return Vector3.zero();
return hit.fill(this, ray, tnear, depth);
}
// Calculate a surfaces color and search for visible light sources
Vector3 traceLight(Vector3 baseColor, Vector3 pos, Vector3 normal) {
var outColor = Vector3(0.0, 0.0, 0.0);
for (var o in objects) {
if (o.emissionColor == Vector3.zero()) continue;
var transmission = Vector3(1,1,1);
var lightDirection = (o.center - pos)..normalize();
var nray = Ray.originDirection(pos + normal * bounceOffset, lightDirection);
for (var io in objects) {
if (io is SphereObject && io != o) {
if (io.intersect(nray) != null) {
transmission = Vector3.zero();
break;
}
}
}
outColor += baseColor.clone()..multiply(transmission * max(0, normal.dot(lightDirection)))..multiply(o.emissionColor);
}
return outColor;
}
}
class RTCamera {
RTScene scene;
double fov;
int width;
int height;
// Cached constants to speed up ray calculation
double invWidth;
double invHeight;
double aspectratio;
double angle;
RTCamera(this.scene, {this.fov = 45, this.width, this.height}) {
invWidth = 1.0 / width;
invHeight = 1.0 / height;
aspectratio = width / height;
angle = tan(pi * 0.5 * fov / 180);
}
Vector3 calcPixel(int x, int y) {
// Calculate perspective and ray direction for given coords
var xx = (2 * ((x + 0.5) * invWidth) - 1) * angle * aspectratio;
var yy = (1 - 2 * ((y + 0.5) * invHeight)) * angle;
var raydir = Vector3(xx, yy, 1.0)..normalize()..applyQuaternion(Quaternion.euler(0, radians(45), 0));
// Trace the scene
return scene.traceFill(Ray.originDirection(Vector3(0.0, 10.0, -22.0), raydir), 0);
}
}
// Convert HSV to linear RGB for the colored orbs
Vector3 hslToRgb(h, s, l) {
double r, g, b;
if (s == 0) {
r = g = b = l;
} else {
double c(double p, double q, double t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = c(p, q, h + 1/3);
g = c(p, q, h);
b = c(p, q, h - 1/3);
}
return Vector3(r, g, b);
}
double linear2sRGB(double c) => (c <= 0.0031308) ? (c * 12.92) : (1.055 * pow( c, 1.0 / 2.4 ) - 0.055);
void main() async {
CanvasElement canvas = querySelector("#canvas");
var ctx = canvas.context2D;
var scene = RTScene();
// The floor
scene.objects.add(SphereObject(Vector3(0.0, -10006, -20), 10000, surfaceColor: Vector3(0.6, 0.6, 0.6)));
// The center reflective orb
scene.objects.add(SphereObject(Vector3(0.0, -1, -10), 2, surfaceColor: Vector3(0.0, 0.0, 0.0), transparency: 0.0, reflection: 1.0));
// The colorful semi reflective orbs
for (int i = 0; i < 10; i++) {
var dt = radians(360 * i / 10);
scene.objects.add(SphereObject(Vector3(sin(dt) * 4, -1, cos(dt) * 4 - 10), 1, surfaceColor: hslToRgb(i / 10, 0.7, 0.7), reflection: 0.2));
}
// The colorful outer matte orbs
for (int i = 0; i < 10; i++) {
var dt = radians(360 * (i + 0.5) / 10);
scene.objects.add(SphereObject(Vector3(sin(dt) * 6, -1, cos(dt) * 6 - 10), 1, surfaceColor: hslToRgb(i / 10, 0.7, 0.7)));
}
// Red light
scene.objects.add(SphereObject(Vector3(-30, 20, -10), 3, surfaceColor: Vector3.zero(), emissionColor: Vector3(1.0, 0.3, 0.3)));
// Center white light
scene.objects.add(SphereObject(Vector3(0, 20, -10), 3, surfaceColor: Vector3.zero(), emissionColor: Vector3(0.7, 0.7, 0.7)));
// Blue light
scene.objects.add(SphereObject(Vector3(30, 20, -10), 3, surfaceColor: Vector3.zero(), emissionColor: Vector3(0.3, 0.3, 1.0)));
const aa = 2; // Supersampling rate
// We render to this internal buffer and then downscale it into the source image
var img = ctx.createImageData(canvas.width * aa, canvas.height * aa);
var done = Uint8List(img.width * img.height * aa);
var imgcv = CanvasElement(width: img.width, height: img.height);
var camera = RTCamera(scene, width: img.width, height: img.height);
ctx.scale(1 / aa, 1 / aa); // Downscale everything applied to visible canvas
for (int i = 0; i < 10; i++) {
var step = 1 << (9 - i); // Progressively fill in pixels, increasing in detail
var n = 0;
for (int y = 0; y < img.height; y += step) {
for (int x = 0; x < img.width; x += step) {
if (done[x + y * img.width] != 0) continue;
done[x + y * img.width] = 1;
// Calculate pixel at position
var pixel = camera.calcPixel(x, y);
// Tonemapping
var r = min(255, max(0, (linear2sRGB(pixel.x) * 255).round()));
var g = min(255, max(0, (linear2sRGB(pixel.y) * 255).round()));
var b = min(255, max(0, (linear2sRGB(pixel.z) * 255).round()));
// Fill in block
for (int sx = 0; sx < step && x + sx < img.width; sx++) {
for (int sy = 0; sy < step && y + sy < img.width; sy++) {
int iof = (x + sx + (y + sy) * img.width) * 4;
img.data[iof + 0] = r;
img.data[iof + 1] = g;
img.data[iof + 2] = b;
img.data[iof + 3] = 255;
}
}
// Wait for next frame after 16384 pixels to prevent the browser from hanging
if (n++ > 0x4000) {
n = 0;
await window.animationFrame;
imgcv.context2D.putImageData(img, 0, 0);
ctx.drawImage(imgcv, 0, 0);
}
}
}
// Draw the internal buffer to the canvas element, downscaled
imgcv.context2D.putImageData(img, 0, 0);
ctx.drawImage(imgcv, 0, 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment