aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html259
-rw-r--r--package-lock.json32
-rw-r--r--package.json3
-rw-r--r--src/Logger.ts7
-rw-r--r--src/RaytraceDispatcher.ts9
-rw-r--r--src/index.ts232
-rw-r--r--src/models/Scene.ts92
-rw-r--r--src/ui/benchmarkCharts.ts90
-rw-r--r--src/ui/benchmarkView.ts111
-rw-r--r--src/ui/demoView.ts141
-rw-r--r--src/ui/index.ts51
-rw-r--r--style.css2
-rw-r--r--webpack.config.js2
13 files changed, 691 insertions, 340 deletions
diff --git a/index.html b/index.html
index 52499cf..2295c32 100644
--- a/index.html
+++ b/index.html
@@ -9,6 +9,7 @@
<body>
<div class="container">
+
<div class="columns">
<div class="column col-xl-12">
<div id="output-wrapper" class="p-centered">
@@ -16,119 +17,179 @@
</div>
</div>
</div>
- <div class="container grid-xl">
- <div class="columns controls">
- <div class="column">
- <div class="columns">
- <div class="column col-3">
- <p>A minimal ray tracing engine written from scratch in JS. No graphics APIs or libraries are used, only a single HTML5 canvas call to draw the generated bitmap image.</p>
- <p>Various effects are supported, including recursive optical reflections and refractions, and <a href="https://en.wikipedia.org/wiki/Phong_reflection_model">Phong shading</a>. Basic multi-threading is implemented using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank">Web Workers API</a>.</p>
- <p>The source is on <a href="https://github.com/jamesbarnett91/js-raytracer" target="_blank">GitHub</a>.</p>
- </div>
- <div class="divider-vert"></div>
- <div class="column col-2">
- <span>Render options</span>
- <div class="form-group">
- <label class="form-switch">
- <input id="diffuse-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Diffuse lighting
- </label>
- </div>
- <div class="form-group">
- <label class="form-switch">
- <input id="specular-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Specular lighting
- </label>
+
+ <div class="container grid-xl">
+
+ <div class="columns">
+ <div class="column">
+ <ul class="tab">
+ <li id="demo-tab" class="tab-item active">
+ <a href="#">Raytracing demo</a>
+ </li>
+ <li id="benchmark-tab" class="tab-item">
+ <a href="#">CPU Benchmark</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="demo-mode" class="columns controls">
+ <div class="column">
+ <div class="columns">
+ <div class="column col-3">
+ <p>A minimal ray tracing engine written from scratch in JS. No graphics APIs or libraries are used, only a single HTML5 canvas call to draw the generated bitmap image.</p>
+ <p>Various effects are supported, including recursive optical reflections and refractions, and <a href="https://en.wikipedia.org/wiki/Phong_reflection_model">Phong shading</a>. Basic multi-threading is implemented using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank">Web Workers API</a>.</p>
+ <p>The raytracer can also produce CPU performance benchmarks.</p>
+ <p>The source is on <a href="https://github.com/jamesbarnett91/js-raytracer" target="_blank">GitHub</a>.</p>
</div>
- <div class="form-group">
- <label class="form-switch">
- <input id="shadows-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Shadows
- </label>
+ <div class="divider-vert"></div>
+ <div class="column col-2">
+ <span>Render options</span>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="diffuse-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Diffuse lighting
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="specular-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Specular lighting
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="shadows-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Shadows
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="reflections-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Reflections
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="refractions-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Refractions
+ </label>
+ </div>
</div>
- <div class="form-group">
- <label class="form-switch">
- <input id="reflections-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Reflections
- </label>
+ <div class="column col-3">
+ <span>Performance options</span>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="direct-transfer" type="checkbox" checked>
+ <i class="form-icon"></i> Zero-copy array transfer
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="enable-threads-toggle" type="checkbox" checked>
+ <i class="form-icon"></i> Multi-threaded rendering
+ </label>
+ </div>
+ <div class="form-group nested-slider">
+ <label class="form-label label-sm" for="threads">Render threads</label>
+ <!-- TODO style -->
+ <div class="input-group">
+ <input id="threads" class="slider" type="range" min="2" max="32" value="4" step="2">
+ <span id="threads-value" class="input-group-addon">4</span>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="form-group">
+ <label id="chunk-size-label" class="form-label label-sm" for="res">Chunk size</label>
+ <select id="chunk-size" class="form-select select-sm">
+ <option value="0" selected>Auto</option>
+ <option value="8">8x8</option>
+ <option value="16">16x16</option>
+ <option value="32">32x32</option>
+ <option value="64">64x64</option>
+ <option value="128">128x128</option>
+ <option value="256">256x256</option>
+ </select>
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-group">
+ <label id="chunk-allocation-mode-label" class="form-label label-sm" for="res">Chunk allocation mode</label>
+ <select id="chunk-allocation-mode" class="form-select select-sm">
+ <option value="SEQUENTIAL" selected>Sequential</option>
+ <option value="RANDOM">Random</option>
+ <option value="CENTER_TO_EDGE">Center to edge</option>
+ <option value="EDGE_TO_CENTER">Edge to center</option>
+ </select>
+ </label>
+ </div>
</div>
- <div class="form-group">
- <label class="form-switch">
- <input id="refractions-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Refractions
- </label>
+ <div class="column col-4">
+ <div class="form-group">
+ <label id="res-label" class="form-label label-sm" for="res">Resolution</label>
+ <select id="res" class="form-select select-sm">
+ <option value="360p">360p</option>
+ <option value="480p">480p</option>
+ <option value="720p" selected >720p</option>
+ <option value="1080p">1080p</option>
+ <option value="1440p">1440p</option>
+ <option value="4k">4k</option>
+ </select>
+ </div>
+ <button id="render" class="btn btn-primary btn-lg">Render</button>
+ <button id="stop-render" class="btn btn-link d-hide">Stop render</button>
+ <button id="view-full" class="btn btn-link d-hide">View full image</button>
+ <pre class="code"><code id="console-demo" class="console"></code></pre>
</div>
</div>
- <div class="column col-3">
- <span>Performance options</span>
- <div class="form-group">
- <label class="form-switch">
- <input id="direct-transfer" type="checkbox" checked>
- <i class="form-icon"></i> Zero-copy array transfer
- </label>
+ </div>
+ </div>
+
+ <div id="benchmark-mode" class="columns controls d-hide">
+ <div class="column">
+ <div class="columns">
+ <div class="column col-3">
+ <p>You can also use the raytracer to run a CPU and JavaScript performance benchmark.</p>
+ <p>This will run multiple 1080p renders, with both single and multicore workloads. An overall score will be calculated based on performance, higher is better.</p>
+ <p>Compare your results to the reference scores from the various other devices I've tested.</p>
</div>
- <div class="form-group">
- <label class="form-switch">
- <input id="enable-threads-toggle" type="checkbox" checked>
- <i class="form-icon"></i> Multi-threaded rendering
- </label>
+ <div class="divider-vert"></div>
+ <div class="column col-6">
+ <div id="benchmark-results-graph" style="width: 100%; height: 500px"></div>
</div>
- <div class="form-group nested-slider">
- <label class="form-label label-sm" for="threads">Render threads</label>
- <!-- TODO style -->
- <div class="input-group">
- <input id="threads" class="slider" type="range" min="2" max="32" value="4" step="2">
- <span id="threads-value" class="input-group-addon">4</span>
+ <div class="column col-3">
+ <div class="columns">
+ <div class="column col-12 d-flex" style="justify-content: center; padding-bottom: 15px">
+ <button id="benchmark" class="btn btn-primary btn-lg">Run benchmark</button>
+ </div>
+ </div>
+ <div class="columns">
+ <div class="column col-12" >
+ <div class="h4">Score</div>
+ </div>
+ </div>
+ <div class="columns">
+ <div class="column col-6">
+ <div>Multi-core</div>
+ <div id="multi-core-score" class="h1 loading-lg">-</div>
+ </div>
+ <div class="column col-6">
+ <div>Single core</div>
+ <div id="single-core-score" class="h1 loading-lg">-</div>
+ </div>
+ </div>
+ <div class="columns">
+ <div class="column col-12" >
+ <pre class="code"><code id="console-benchmark" class="console"></code></pre>
+ </div>
</div>
</div>
- <div class="form-group">
- <label class="form-group">
- <label id="chunk-size-label" class="form-label label-sm" for="res">Chunk size</label>
- <select id="chunk-size" class="form-select select-sm">
- <option value="0" selected>Auto</option>
- <option value="8">8x8</option>
- <option value="16">16x16</option>
- <option value="32">32x32</option>
- <option value="64">64x64</option>
- <option value="128">128x128</option>
- <option value="256">256x256</option>
- </select>
- </label>
- </div>
- <div class="form-group">
- <label class="form-group">
- <label id="chunk-allocation-mode-label" class="form-label label-sm" for="res">Chunk allocation mode</label>
- <select id="chunk-allocation-mode" class="form-select select-sm">
- <option value="SEQUENTIAL" selected>Sequential</option>
- <option value="RANDOM">Random</option>
- <option value="CENTER_TO_EDGE">Center to edge</option>
- <option value="EDGE_TO_CENTER">Edge to center</option>
- </select>
- </label>
- </div>
- </div>
- <div class="column col-4">
- <div class="form-group">
- <label id="res-label" class="form-label label-sm" for="res">Resolution</label>
- <select id="res" class="form-select select-sm">
- <option value="360p">360p</option>
- <option value="480p">480p</option>
- <option value="720p" selected >720p</option>
- <option value="1080p">1080p</option>
- <option value="1440p">1440p</option>
- <option value="4k">4k</option>
- </select>
- </div>
- <button id="render" class="btn btn-primary">Render</button>
- <button id="stop-render" class="btn btn-link d-hide">Stop render</button>
- <button id="view-full" class="btn btn-link d-hide">View full image</button>
- <pre class="code"><code id="console"></code></pre>
</div>
</div>
</div>
+
</div>
</div>
-
<script src="./bundle.js"></script>
</body>
</html>
diff --git a/package-lock.json b/package-lock.json
index 9203576..0897065 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "GPL-3.0",
"dependencies": {
"class-transformer": "^0.5.1",
+ "echarts": "^5.6.0",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
@@ -2027,6 +2028,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
+ "node_modules/echarts/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -6695,6 +6712,21 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ },
+ "node_modules/zrender/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
}
}
}
diff --git a/package.json b/package.json
index b876c60..6a73562 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,8 @@
},
"dependencies": {
"class-transformer": "^0.5.1",
- "reflect-metadata": "^0.2.2"
+ "reflect-metadata": "^0.2.2",
+ "echarts": "^5.6.0"
},
"engines": {
"node": ">=10.0.0"
diff --git a/src/Logger.ts b/src/Logger.ts
index 4087e91..5a825d0 100644
--- a/src/Logger.ts
+++ b/src/Logger.ts
@@ -1,10 +1,9 @@
export class Logger {
- constructor() {}
+ constructor(readonly element: HTMLElement) {}
log(message: string) {
- const elem = document.getElementById('console')!;
- elem.innerText += `${message}\n`;
- elem.scrollTop = elem.scrollHeight;
+ this.element.innerText += `${message}\n`;
+ this.element.scrollTop = this.element.scrollHeight;
console.log(message);
}
}
diff --git a/src/RaytraceDispatcher.ts b/src/RaytraceDispatcher.ts
index eeae5ce..12f2502 100644
--- a/src/RaytraceDispatcher.ts
+++ b/src/RaytraceDispatcher.ts
@@ -90,8 +90,13 @@ export class RaytraceDispatcher {
}
if (this.completedWorkers == this.context.options.numThreads) {
- this.onComplete();
- this.logger.log(`Raytrace completed in ${new Date().getTime() - this.renderStartMs}ms\n`);
+ const renderTimeMs = (new Date().getTime() - this.renderStartMs);
+
+ this.logger.log(`Raytrace completed in ${renderTimeMs}ms\n`);
+
+ const pixels = this.context.width * this.context.height;
+ const score = Math.round(pixels/renderTimeMs);
+ this.onComplete(score);
}
}
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 3dab80f..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import {Colour} from './models/Colour';
-import {Framebuffer} from './Framebuffer';
-import {Plane, Sphere} from './models/Geometry';
-import {Light} from './models/Light';
-import {Albedo, Material} from './models/Material';
-import {RaytraceDispatcher} from './RaytraceDispatcher';
-import {ChunkAllocationMode, RaytraceContext, RaytracerOptions} from './models/RaytraceContext';
-import {Vector} from './models/Vector';
-import {Logger} from './Logger';
-
-let dispatcher: RaytraceDispatcher;
-
-function render() {
- getRenderButton().classList.add('loading');
- getStopRenderButton().classList.remove('d-hide');
- getViewFullButton().classList.add('d-hide');
- dispatcher = initDispatcher(parseOptions());
- dispatcher.requestRender();
-}
-
-function stopRender() {
- dispatcher.stopRender();
- onRenderComplete();
-}
-
-function initDispatcher(options: RaytracerOptions): RaytraceDispatcher {
- const {width, height} = parseResolution();
-
- const fov = Math.PI / 3;
-
- const framebuffer = new Framebuffer(width, height);
-
- const materialMirror = new Material(
- new Colour(220, 220, 220),
- new Albedo(0.1, 1, 0.8, 0.0),
- 2500,
- 1
- );
-
- const materialGlass = new Material(
- new Colour(153, 179, 204),
- new Albedo(0.0, 0.5, 0.1, 0.8),
- 125,
- 1.5
- );
-
- const spheres: Sphere[] = [
- new Sphere(1, new Vector(-6.5, -3, -26), matteMaterial(27, 118, 255)),
- new Sphere(1, new Vector(-14, -3, -25), matteMaterial(146, 80, 188)),
- new Sphere(1, new Vector(-10, -3, -25), matteMaterial(0, 146, 178)),
- new Sphere(1, new Vector(-10, -3, -20), matteMaterial(185, 18, 27)),
- new Sphere(1, new Vector(-2.5, -3, -20), matteMaterial(115, 45, 217)),
- new Sphere(2, new Vector(-10.5, -2, -16), materialMirror),
- new Sphere(1, new Vector(-3, -3, -16), matteMaterial(247, 178, 173)),
- new Sphere(1, new Vector(-6, -3, -18), matteMaterial(154, 188, 167)),
- new Sphere(1, new Vector(-6, -3, -12), matteMaterial(96, 125, 139)),
- new Sphere(1, new Vector(-9.5, -3, -12), matteMaterial(122, 186, 242)),
- new Sphere(1, new Vector(0, -3, -14), matteMaterial(250, 91, 15)),
- new Sphere(1, new Vector(-3, -3, -11), materialGlass),
- new Sphere(1, new Vector(-1, -3, -10), matteMaterial(54, 95, 182)),
- new Sphere(1, new Vector(-4.5, -3, -8), matteMaterial(139, 195, 74)),
-
- new Sphere(4, new Vector(4, 0, -18), materialMirror),
- new Sphere(1, new Vector(4, -3, -12), matteMaterial(115, 45, 217)),
- new Sphere(1.5, new Vector(8.5, -2.5, -10), materialMirror),
- new Sphere(1, new Vector(1, -3, -11.5), matteMaterial(255, 200, 0)),
- new Sphere(1, new Vector(1.2, -3, -8.2), materialGlass),
- new Sphere(1, new Vector(4, -3, -7), matteMaterial(244, 67, 54)),
- new Sphere(1, new Vector(5.5, -3, -9.5), matteMaterial(150, 237, 137)),
- new Sphere(1, new Vector(6.5, -3, -15), matteMaterial(14, 234, 255)),
- new Sphere(1, new Vector(10, -3, -16), matteMaterial(171, 71, 188)),
- new Sphere(1, new Vector(11, -3, -20), matteMaterial(190, 189, 191)),
- ];
-
- const planes = [
- new Plane(
- -4,
- 50,
- 40,
- -45,
- 1.5,
- new Colour(116, 101, 87),
- new Colour(92, 78, 70)
- ),
- ];
-
- const lights = [
- new Light(new Vector(30, 50, 40), 2.5),
- new Light(new Vector(-20, 50, -25), 0.5),
- ];
-
- const context = new RaytraceContext(
- height,
- width,
- fov,
- spheres,
- planes,
- lights,
- new Colour(221, 221, 221),
- options
- );
-
- return new RaytraceDispatcher(
- framebuffer,
- context,
- new Logger(),
- onRenderComplete
- );
-}
-
-function parseOptions(): RaytracerOptions {
- return {
- numThreads: getDesiredThreadCount(),
- shadows: getInputElement('shadows-toggle').checked,
- diffuseLighting: getInputElement('diffuse-toggle').checked,
- specularLighting: getInputElement('specular-toggle').checked,
- reflections: getInputElement('reflections-toggle').checked,
- refractions: getInputElement('refractions-toggle').checked,
- maxRecurseDepth: 5,
- maxDrawDistance: 1000,
- directMemoryTransfer: getInputElement('direct-transfer').checked,
- chunkSize: parseInt(getInputElement('chunk-size').value, 10),
- chunkAllocationMode: getChunkAllocationMode()
- };
-}
-
-function parseResolution(): {width: number; height: number} {
- switch (getInputElement('res').value) {
- case '360p':
- return {width: 640, height: 360};
- case '480p':
- return {width: 832, height: 480};
- case '720p':
- default:
- return {width: 1280, height: 720};
- case '1080p':
- return {width: 1920, height: 1080};
- case '1440p':
- return {width: 2560, height: 1440};
- case '4k':
- return {width: 3840, height: 2160};
- }
-}
-
-function getChunkAllocationMode(): ChunkAllocationMode {
- switch (getInputElement('chunk-allocation-mode').value) {
- case 'SEQUENTIAL':
- default:
- return ChunkAllocationMode.SEQUENTIAL;
- case 'RANDOM':
- return ChunkAllocationMode.RANDOM
- case 'CENTER_TO_EDGE':
- return ChunkAllocationMode.CENTER_TO_EDGE
- case 'EDGE_TO_CENTER':
- return ChunkAllocationMode.EDGE_TO_CENTER
- }
-}
-
-function registerEventListeners() {
- getRenderButton().addEventListener('click', render);
- getStopRenderButton().addEventListener('click', stopRender);
-
- getViewFullButton().addEventListener('click', () => {
- const canvas = document.getElementById(
- 'render-output'
- ) as HTMLCanvasElement;
- window.open()!.document.body.innerHTML = `<img src=${canvas.toDataURL()}>`;
- });
-
- const threadsSlider = getInputElement('threads');
- threadsSlider.addEventListener('input', () => {
- getInputElement('threads-value').textContent = threadsSlider.value;
- });
-
- const threadToggle = getInputElement('enable-threads-toggle');
- threadToggle.addEventListener('change', () => {
- threadsSlider.disabled = !threadToggle.checked;
- });
-}
-
-function getDesiredThreadCount(): number {
- if (getInputElement('enable-threads-toggle').checked) {
- return Number.parseInt(getInputElement('threads').value);
- } else {
- return 1;
- }
-}
-
-function onRenderComplete() {
- getRenderButton().classList.remove('loading');
- getStopRenderButton().classList.add('d-hide');
- getViewFullButton().classList.remove('d-hide');
-}
-
-function getRenderButton(): HTMLElement {
- return document.getElementById('render')!;
-}
-
-function getStopRenderButton(): HTMLElement {
- return document.getElementById('stop-render')!;
-}
-
-function getViewFullButton(): HTMLElement {
- return document.getElementById('view-full')!;
-}
-
-function getInputElement(elementId: string) {
- return document.getElementById(elementId) as HTMLInputElement;
-}
-
-function matteMaterial(r: number, g: number, b: number): Material {
- return new Material(
- new Colour(r, g, b),
- new Albedo(0.4, 0.0, 0.0, 0.0),
- 0,
- 1
- );
-}
-
-function glossMaterial(r: number, g: number, b: number): Material {
- return new Material(
- new Colour(r, g, b),
- new Albedo(0.4, 0.1, 0.05, 0),
- 250,
- 1
- );
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- registerEventListeners();
- render();
-});
diff --git a/src/models/Scene.ts b/src/models/Scene.ts
new file mode 100644
index 0000000..31e84bf
--- /dev/null
+++ b/src/models/Scene.ts
@@ -0,0 +1,92 @@
+import {Plane, Sphere} from "./Geometry";
+import {Light} from "./Light";
+import {Albedo, Material} from "./Material";
+import {Colour} from "./Colour";
+import {Vector} from "./Vector";
+
+export function getScene(): {
+ spheres: Sphere[];
+ planes: Plane[];
+ lights: Light[];
+} {
+ return {
+ spheres: [
+ new Sphere(1, new Vector(-6.5, -3, -26), matteMaterial(27, 118, 255)),
+ new Sphere(1, new Vector(-14, -3, -25), matteMaterial(146, 80, 188)),
+ new Sphere(1, new Vector(-10, -3, -25), matteMaterial(0, 146, 178)),
+ new Sphere(1, new Vector(-10, -3, -20), matteMaterial(185, 18, 27)),
+ new Sphere(1, new Vector(-2.5, -3, -20), matteMaterial(115, 45, 217)),
+ new Sphere(2, new Vector(-10.5, -2, -16), mirrorMaterial()),
+ new Sphere(1, new Vector(-3, -3, -16), matteMaterial(247, 178, 173)),
+ new Sphere(1, new Vector(-6, -3, -18), matteMaterial(154, 188, 167)),
+ new Sphere(1, new Vector(-6, -3, -12), matteMaterial(96, 125, 139)),
+ new Sphere(1, new Vector(-9.5, -3, -12), matteMaterial(122, 186, 242)),
+ new Sphere(1, new Vector(0, -3, -14), matteMaterial(250, 91, 15)),
+ new Sphere(1, new Vector(-3, -3, -11), glassMaterial()),
+ new Sphere(1, new Vector(-1, -3, -10), matteMaterial(54, 95, 182)),
+ new Sphere(1, new Vector(-4.5, -3, -8), matteMaterial(139, 195, 74)),
+ new Sphere(4, new Vector(4, 0, -18), mirrorMaterial()),
+ new Sphere(1, new Vector(4, -3, -12), matteMaterial(115, 45, 217)),
+ new Sphere(1.5, new Vector(8.5, -2.5, -10), mirrorMaterial()),
+ new Sphere(1, new Vector(1, -3, -11.5), matteMaterial(255, 200, 0)),
+ new Sphere(1, new Vector(1.2, -3, -8.2), glassMaterial()),
+ new Sphere(1, new Vector(4, -3, -7), matteMaterial(244, 67, 54)),
+ new Sphere(1, new Vector(5.5, -3, -9.5), matteMaterial(150, 237, 137)),
+ new Sphere(1, new Vector(6.5, -3, -15), matteMaterial(14, 234, 255)),
+ new Sphere(1, new Vector(10, -3, -16), matteMaterial(171, 71, 188)),
+ new Sphere(1, new Vector(11, -3, -20), matteMaterial(190, 189, 191)),
+ ],
+ planes: [
+ new Plane(
+ -4,
+ 50,
+ 40,
+ -45,
+ 1.5,
+ new Colour(116, 101, 87),
+ new Colour(92, 78, 70)
+ ),
+ ],
+ lights: [
+ new Light(new Vector(30, 50, 40), 2.5),
+ new Light(new Vector(-20, 50, -25), 0.5),
+ ]
+ }
+}
+
+function mirrorMaterial(): Material {
+ return new Material(
+ new Colour(220, 220, 220),
+ new Albedo(0.1, 1, 0.8, 0.0),
+ 2500,
+ 1
+ );
+}
+
+function glassMaterial(): Material {
+ return new Material(
+ new Colour(153, 179, 204),
+ new Albedo(0.0, 0.5, 0.1, 0.8),
+ 125,
+ 1.5
+ );
+}
+
+function matteMaterial(r: number, g: number, b: number): Material {
+ return new Material(
+ new Colour(r, g, b),
+ new Albedo(0.4, 0.0, 0.0, 0.0),
+ 0,
+ 1
+ );
+}
+
+function glossMaterial(r: number, g: number, b: number): Material {
+ return new Material(
+ new Colour(r, g, b),
+ new Albedo(0.4, 0.1, 0.05, 0),
+ 250,
+ 1
+ );
+}
+
diff --git a/src/ui/benchmarkCharts.ts b/src/ui/benchmarkCharts.ts
new file mode 100644
index 0000000..85513c2
--- /dev/null
+++ b/src/ui/benchmarkCharts.ts
@@ -0,0 +1,90 @@
+import * as echarts from 'echarts';
+import {EChartsOption, EChartsType} from "echarts";
+
+let scoreChart: EChartsType;
+const userDeviceLabel = 'Your device';
+
+let deviceScores = [
+ { device: 'AMD Ryzen 7\n3700X\n(8c/16t)', multiCoreScore: 416, singleCoreScore: 74 },
+ { device: 'Intel 11th Gen\ni5-1145G7\n(4c/8t)', multiCoreScore: 294, singleCoreScore: 73 },
+ { device: 'Intel 8th Gen\ni7-8550U\n(4c/8t)', multiCoreScore: 221, singleCoreScore: 53 },
+ { device: 'iPhone XS\n(6c/6t)\n(*WebKit)', multiCoreScore: 71, singleCoreScore: 28 },
+ { device: 'Raspberry Pi4\nARM Cortex-A72\n(4c/4t)', multiCoreScore: 25, singleCoreScore: 9 }
+]
+
+let scoreDataset = {
+ dimensions: ['device', 'multiCoreScore', 'singleCoreScore'],
+ source: deviceScores
+}
+
+const option: EChartsOption = {
+ legend: {},
+ dataset: scoreDataset,
+ xAxis: { type: 'value' },
+ yAxis: {
+ type: 'category',
+ inverse: true,
+ axisLabel: {
+ formatter: function (label) {
+ if (label == userDeviceLabel) {
+ return `{usersDevice|${label}}`;
+ }
+ return label;
+ },
+ rich: {
+ usersDevice: {
+ color: "#5755d9",
+ fontWeight: "bold",
+ fontSize: "14px"
+ },
+ },
+ },
+ },
+ series: [
+ {
+ name: 'Multi-core',
+ type: 'bar',
+ label: {
+ show: true
+ },
+ },
+ {
+ name: 'Single core',
+ type: 'bar',
+ label: {
+ show: true
+ },
+ }
+ ],
+ grid: {
+ left: '0%',
+ top: '5%',
+ containLabel: true
+ },
+ color: [
+ '#5755d9',
+ '#f1f1fc'
+ ],
+}
+
+export function initScoreChart(dom: HTMLElement) {
+ scoreChart = echarts.init(dom);
+ scoreChart.setOption(option);
+
+ window.addEventListener('resize', function() {
+ scoreChart.resize();
+ });
+}
+
+export function addDeviceResult(multiCoreScore: number, singleCoreScore: number) {
+ // Remove previous result if present
+ const index = deviceScores.findIndex(d => d.device === userDeviceLabel);
+ if (index >= 0) {
+ deviceScores.splice(index, 1)
+ }
+
+ deviceScores.push({device: userDeviceLabel, multiCoreScore: multiCoreScore, singleCoreScore: singleCoreScore});
+ deviceScores.sort((a, b) => b.multiCoreScore - a.multiCoreScore);
+
+ scoreChart.setOption(option);
+} \ No newline at end of file
diff --git a/src/ui/benchmarkView.ts b/src/ui/benchmarkView.ts
new file mode 100644
index 0000000..487ec63
--- /dev/null
+++ b/src/ui/benchmarkView.ts
@@ -0,0 +1,111 @@
+import {ChunkAllocationMode, RaytraceContext, RaytracerOptions} from "../models/RaytraceContext";
+import {RaytraceDispatcher} from "../RaytraceDispatcher";
+import {Framebuffer} from "../Framebuffer";
+import {getScene} from "../models/Scene";
+import {Colour} from "../models/Colour";
+import {Logger} from "../Logger";
+import * as BenchmarkChart from "./benchmarkCharts";
+
+let multiCoreDispatcher: RaytraceDispatcher;
+let singleCoreDispatcher: RaytraceDispatcher;
+
+let multiCoreScore = 0;
+let singleCoreScore = 0;
+
+const deviceAvailableThreadCount = navigator.hardwareConcurrency;
+
+const logger = new Logger(document.getElementById('console-benchmark')!);
+
+const benchmarkButton = document.getElementById('benchmark')!;
+const multiCoreScoreElement = document.getElementById('multi-core-score')!;
+const singleCoreScoreElement = document.getElementById('single-core-score')!;
+
+export function registerEventListeners() {
+ benchmarkButton.addEventListener('click', startBenchmark);
+ logger.log(`Detected ${deviceAvailableThreadCount} available CPU threads, ready to run`);
+}
+
+export function initChart() {
+ BenchmarkChart.initScoreChart(document.getElementById('benchmark-results-graph')!);
+}
+
+fu