aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html28
-rw-r--r--src/Framebuffer.ts10
-rw-r--r--src/RaytraceContext.ts2
-rw-r--r--src/RaytraceDispatcher.ts32
-rw-r--r--src/Raytracer.ts25
-rw-r--r--src/index.ts29
-rw-r--r--style.css10
7 files changed, 108 insertions, 28 deletions
diff --git a/index.html b/index.html
index c74d2a7..a08fcb9 100644
--- a/index.html
+++ b/index.html
@@ -24,6 +24,16 @@
<div class="divider"></div>
</div> -->
<div class="column col-12 col-xl-9">
+ <div class="form-group">
+ <label class="form-label label-sm" for="res">Resolution</label>
+ <select id="res" class="form-select select-sm">
+ <option value="720p" selected>720p</option>
+ <option value="1080p">1080p</option>
+ <option value="1440p">1440p</option>
+ <option value="4k">4k</option>
+ <option value="8k">8k</option>
+ </select>
+ </div>
<p>Render options</p>
<div class="form-group">
<label class="form-switch">
@@ -49,21 +59,35 @@
<i class="form-icon"></i> Reflections
</label>
</div>
+ <p>Performance options</p>
+ <div class="form-group">
+ <label class="form-switch tooltip">
+ <input id="buffer-draw" type="checkbox" checked>
+ <i class="form-icon"></i> Buffer draw calls
+ </label>
+ </div>
+ <div class="form-group">
+ <label class="form-switch">
+ <input id="direct-transfer" type="checkbox" checked>
+ <i class="form-icon"></i> Direct memory 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 thread-slider">
+ <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="16" value="2" step="2">
+ <input id="threads" class="slider" type="range" min="2" max="12" value="2" step="2">
<span id="threads-value" class="input-group-addon">2</span>
</div>
</div>
<button id="render" class="btn btn-primary">Render</button>
+ <button id="view-full" class="btn btn-link">view full size</button>
<pre class="code"><code id="console"></code></pre>
</div>
</div>
diff --git a/src/Framebuffer.ts b/src/Framebuffer.ts
index b9926d2..a6ad5fc 100644
--- a/src/Framebuffer.ts
+++ b/src/Framebuffer.ts
@@ -1,5 +1,3 @@
-import {Colour} from './Colour';
-
export class Framebuffer {
readonly width: number;
readonly height: number;
@@ -24,11 +22,11 @@ export class Framebuffer {
);
}
- writePixelAt(x: number, y: number, colour: Colour) {
+ writePixelAt(x: number, y: number, r: number, g: number, b: number) {
const startIdx = (y * this.width + x) * 4;
- this.canvasImageData.data[startIdx] = colour.r;
- this.canvasImageData.data[startIdx + 1] = colour.g;
- this.canvasImageData.data[startIdx + 2] = colour.b;
+ this.canvasImageData.data[startIdx] = r;
+ this.canvasImageData.data[startIdx + 1] = g;
+ this.canvasImageData.data[startIdx + 2] = b;
this.canvasImageData.data[startIdx + 3] = 255; // No A
}
diff --git a/src/RaytraceContext.ts b/src/RaytraceContext.ts
index 3629f5a..34f20dd 100644
--- a/src/RaytraceContext.ts
+++ b/src/RaytraceContext.ts
@@ -32,4 +32,6 @@ export interface RaytracerOptions {
reflections: boolean;
maxRecurseDepth: number;
maxDrawDistance: number;
+ bufferDrawCalls: boolean;
+ directMemoryTransfer: boolean;
}
diff --git a/src/RaytraceDispatcher.ts b/src/RaytraceDispatcher.ts
index 1ce412f..be36bb1 100644
--- a/src/RaytraceDispatcher.ts
+++ b/src/RaytraceDispatcher.ts
@@ -1,4 +1,3 @@
-import {Colour} from './Colour';
import {Framebuffer} from './Framebuffer';
import {RaytraceContext} from './RaytraceContext';
import {instanceToPlain} from 'class-transformer';
@@ -7,7 +6,8 @@ import {Logger} from './Logger';
export class RaytraceDispatcher {
private renderStartMs: number;
- private responsesReceived = 0;
+ private completedThreads = 0;
+ private processedResponses = 0;
constructor(
readonly framebuffer: Framebuffer,
readonly context: RaytraceContext,
@@ -18,7 +18,7 @@ export class RaytraceDispatcher {
}
requestRender() {
- // Assumes height and threads are always even
+ // Assumes height%threads = 0
const rowBatchSize = this.context.height / this.context.options.numThreads;
for (let y = 0; y < this.context.height; y += rowBatchSize) {
@@ -55,7 +55,8 @@ export class RaytraceDispatcher {
}
private handleWorkerComplete() {
- if (++this.responsesReceived === this.context.options.numThreads) {
+ if (++this.completedThreads === this.context.options.numThreads) {
+ this.framebuffer.flush();
this.logger.log(
`Raytrace completed in ${new Date().getTime() - this.renderStartMs}ms`
);
@@ -63,10 +64,25 @@ export class RaytraceDispatcher {
}
}
- private processResponse(rowIndex: number, rowData: Colour[]) {
- for (let x = 0; x < this.framebuffer.width; x++) {
- this.framebuffer.writePixelAt(x, rowIndex, rowData[x]);
+ private processResponse(rowIndex: number, rowData: ArrayBuffer) {
+ const clampedRowData = new Uint8ClampedArray(rowData);
+
+ for (let x = 0; x < this.context.width; x++) {
+ const idx = x * 3;
+ const r = clampedRowData[idx];
+ const g = clampedRowData[idx + 1];
+ const b = clampedRowData[idx + 2];
+ this.framebuffer.writePixelAt(x, rowIndex, r, g, b);
+ }
+
+ if (
+ this.context.options.bufferDrawCalls &&
+ ++this.processedResponses >= this.context.height * 0.05
+ ) {
+ this.framebuffer.flush();
+ this.processedResponses = 0;
+ } else if (!this.context.options.bufferDrawCalls) {
+ this.framebuffer.flush();
}
- this.framebuffer.flush();
}
}
diff --git a/src/Raytracer.ts b/src/Raytracer.ts
index 68d5138..673977b 100644
--- a/src/Raytracer.ts
+++ b/src/Raytracer.ts
@@ -49,7 +49,7 @@ class Raytracer {
process() {
for (let y = this.rowStartIndex; y <= this.rowEndIndex; y++) {
- const rowResultBuffer: Colour[] = [];
+ const resultBuffer = new Uint8ClampedArray(3 * this.context.width);
for (let x = 0; x < this.context.width; x++) {
const rayX = x + 0.5 - this.context.width / 2;
@@ -61,14 +61,24 @@ class Raytracer {
const pixelValue = this.raytrace(ray);
- rowResultBuffer.push(pixelValue);
+ const buffIdx = x * 3;
+ resultBuffer[buffIdx] = pixelValue.r;
+ resultBuffer[buffIdx + 1] = pixelValue.g;
+ resultBuffer[buffIdx + 2] = pixelValue.b;
}
- self.postMessage({
- type: 'raytraceResultRow',
- resultBuffer: rowResultBuffer,
- rowIndex: y,
- });
+ if (this.context.options.directMemoryTransfer) {
+ // prettier-ignore
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ self.postMessage({type: 'raytraceResultRow', rowIndex: y, resultBuffer: resultBuffer.buffer}, [resultBuffer.buffer]);
+ } else {
+ self.postMessage({
+ type: 'raytraceResultRow',
+ resultBuffer: resultBuffer.buffer,
+ rowIndex: y,
+ });
+ }
}
self.postMessage({type: 'raytraceComplete'});
@@ -205,7 +215,6 @@ class Raytracer {
incidentAngle: Vector,
surfaceNormal: Vector
): Vector {
- // I - N*2.f*(I*N);
// v2 = v1 – 2(v1.n)n https://bocilmania.com/2018/04/21/how-to-get-reflection-vector/
return incidentAngle.subtract(
surfaceNormal
diff --git a/src/index.ts b/src/index.ts
index 3736c92..6b86f57 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,8 +15,8 @@ function render() {
}
function initDispatcher(options: RaytracerOptions): RaytraceDispatcher {
- const width = 960;
- const height = 720;
+ const {width, height} = parseResolution();
+
const fov = Math.PI / 3;
const framebuffer = new Framebuffer(width, height);
@@ -68,12 +68,37 @@ function parseOptions(): RaytracerOptions {
reflections: getInputElement('reflections-toggle').checked,
maxRecurseDepth: 5,
maxDrawDistance: 1000,
+ bufferDrawCalls: getInputElement('buffer-draw').checked,
+ directMemoryTransfer: getInputElement('direct-transfer').checked,
};
}
+function parseResolution(): {width: number; height: number} {
+ switch (getInputElement('res').value) {
+ case '720p':
+ default:
+ return {width: 960, height: 720};
+ case '1080p':
+ return {width: 1440, height: 1080};
+ case '1440p':
+ return {width: 1920, height: 1440};
+ case '4k':
+ return {width: 2880, height: 2160};
+ case '8k':
+ return {width: 5760, height: 4320};
+ }
+}
+
function registerEventListeners() {
getRenderButton().addEventListener('click', render);
+ document.getElementById('view-full')!.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;
diff --git a/style.css b/style.css
index 0403e47..453687e 100644
--- a/style.css
+++ b/style.css
@@ -3,7 +3,7 @@ body {
}
p {
- margin: 0 0 0.8em;
+ margin: 0;
}
#output-wrapper {
@@ -13,6 +13,12 @@ p {
margin-top: 0.5em;
}
+#render-output {
+ width: 960px;
+ height: 720px;
+ object-fit: contain;
+}
+
.controls {
margin-top: 0.5em;
}
@@ -22,7 +28,7 @@ p {
width: 100%;
}
-.thread-slider {
+.nested-slider {
padding-left: 40px;
}