diff options
| -rw-r--r-- | index.html | 259 | ||||
| -rw-r--r-- | package-lock.json | 32 | ||||
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | src/Logger.ts | 7 | ||||
| -rw-r--r-- | src/RaytraceDispatcher.ts | 9 | ||||
| -rw-r--r-- | src/index.ts | 232 | ||||
| -rw-r--r-- | src/models/Scene.ts | 92 | ||||
| -rw-r--r-- | src/ui/benchmarkCharts.ts | 90 | ||||
| -rw-r--r-- | src/ui/benchmarkView.ts | 111 | ||||
| -rw-r--r-- | src/ui/demoView.ts | 141 | ||||
| -rw-r--r-- | src/ui/index.ts | 51 | ||||
| -rw-r--r-- | style.css | 2 | ||||
| -rw-r--r-- | webpack.config.js | 2 |
13 files changed, 691 insertions, 340 deletions
@@ -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')!); +} + +function startBenchmark() { + benchmarkButton.classList.add('loading'); + multiCoreScoreElement.classList.add('loading'); + document.getElementById('single-core-score')!.classList.add('loading'); + + multiCoreDispatcher = initDispatcher( + 1920, + 1080, + getBenchmarkRenderOptions(true), + onMultiCoreBenchmarkComplete + ) + + singleCoreDispatcher = initDispatcher( + 1920, + 1080, + getBenchmarkRenderOptions(false), + onSingleCoreBenchmarkComplete + ) + + logger.log("Started multi-core benchmark"); + multiCoreDispatcher.requestRender(); |