diff --git a/src/App.tsx b/src/App.tsx index 374ac34..4a7eb3c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ const DEFAULT_SETTINGS: Settings = { gamma: 2.2, }, scene: { + seed: Math.floor(Math.random() * 9_999_999), showRabbit: false, sphereCount: 250, }, diff --git a/src/RenderSettings.tsx b/src/RenderSettings.tsx index 5e4fbec..fb3cb8a 100644 --- a/src/RenderSettings.tsx +++ b/src/RenderSettings.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect, useRef, useCallback } from "react"; + export interface Settings { render: { width: number; @@ -10,6 +12,7 @@ export interface Settings { gamma: number; }; scene: { + seed: number; showRabbit: boolean; sphereCount: number; }; @@ -21,11 +24,32 @@ interface RenderSettingsProps { } function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { - const updateRender = (patch: Partial) => - onSettingsChange({ ...settings, render: { ...settings.render, ...patch } }); + const [local, setLocal] = useState(settings); + const timerRef = useRef>(); + + useEffect(() => setLocal(settings), [settings]); + + const debouncedChange = useCallback( + (next: Settings) => { + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => onSettingsChange(next), 300); + }, + [onSettingsChange], + ); - const updateScene = (patch: Partial) => - onSettingsChange({ ...settings, scene: { ...settings.scene, ...patch } }); + useEffect(() => () => clearTimeout(timerRef.current), []); + + const updateRender = (patch: Partial) => { + const next = { ...local, render: { ...local.render, ...patch } }; + setLocal(next); + debouncedChange(next); + }; + + const updateScene = (patch: Partial) => { + const next = { ...local, scene: { ...local.scene, ...patch } }; + setLocal(next); + debouncedChange(next); + }; return (
@@ -38,7 +62,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { updateRender({ samples: e.target.valueAsNumber })} /> @@ -49,7 +73,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { updateRender({ bounces: e.target.valueAsNumber })} /> @@ -60,7 +84,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { updateRender({ focalLength: e.target.valueAsNumber })} /> @@ -71,7 +95,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { updateRender({ focalDistance: e.target.valueAsNumber })} /> @@ -83,7 +107,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { type="number" min={0} step={0.01} - value={settings.render.aperture} + value={local.render.aperture} onChange={(e) => updateRender({ aperture: e.target.valueAsNumber })} /> @@ -95,7 +119,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { type="number" min={1} step={0.1} - value={settings.render.gamma} + value={local.render.gamma} onChange={(e) => updateRender({ gamma: e.target.valueAsNumber })} /> @@ -106,13 +130,25 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) {
Scene
    +
  • + +
  • @@ -122,7 +158,7 @@ function RenderSettings({ settings, onSettingsChange }: RenderSettingsProps) { Rabbit updateScene({ showRabbit: e.target.checked })} /> diff --git a/src/hooks/useRenderer.ts b/src/hooks/useRenderer.ts index 230c4af..def7a71 100644 --- a/src/hooks/useRenderer.ts +++ b/src/hooks/useRenderer.ts @@ -34,7 +34,7 @@ function startRender(canvas: HTMLCanvasElement, settings: Settings): Worker { type: "start", canvas: offscreen, settings: settings.render, - entities: buildSceneData(settings.scene.sphereCount, settings.render.focalDistance), + entities: buildSceneData(settings.scene.sphereCount, settings.render.focalDistance, settings.scene.seed), models: settings.scene.showRabbit ? [RABBIT_MODEL_DATA] : [], gamma: settings.render.gamma, }; diff --git a/src/scene.ts b/src/scene.ts index 3b02c69..ebf0253 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -1,4 +1,4 @@ -import { vec3, mag, sub } from "./utils/math"; +import { vec3, mag, sub, createRng } from "./utils/math"; import { rgb } from "./utils/colour"; import type { SceneObject, ModelData } from "./render.types"; import RABBIT_MODEL from "./models/rabbit"; @@ -81,14 +81,15 @@ export const RABBIT_MODEL_DATA: ModelData = { ior: 1.5, }; -export function buildSceneData(sphereCount: number, focalDistance: number): SceneObject[] { +export function buildSceneData(sphereCount: number, focalDistance: number, seed: number): SceneObject[] { + const random = createRng(seed); const scene: SceneObject[] = [...BASE_SCENE]; for (let i = 0; i < sphereCount; i++) { for (let attempts = 0; attempts < 100; attempts++) { - const radius = 2 + Math.random() * 5; - const x = (Math.random() - 0.5) * 150; - const z = focalDistance + (Math.random() - 0.5) * 175; + const radius = 2 + random() * 5; + const x = (random() - 0.5) * 150; + const z = focalDistance + (random() - 0.5) * 175; const position = vec3(x, MAIN_SIZE - radius, z); let isIntersecting = false; @@ -102,17 +103,17 @@ export function buildSceneData(sphereCount: number, focalDistance: number): Scen } if (!isIntersecting) { - const metallic = Math.random() > 0.6 ? 1.0 : 0.0; - const transmission = !metallic && Math.random() > 0.7 ? 1.0 : 0.0; + const metallic = random() > 0.6 ? 1.0 : 0.0; + const transmission = !metallic && random() > 0.7 ? 1.0 : 0.0; scene.push({ shape: "sphere", radius, position, emission: rgb(0, 0, 0), - albedo: rgb(Math.random(), Math.random(), Math.random()), + albedo: rgb(random(), random(), random()), metallic, - roughness: Math.random(), + roughness: random(), transmission, ior: 1.5, }); diff --git a/src/utils/math.test.ts b/src/utils/math.test.ts index 5b22707..7bb1d13 100644 --- a/src/utils/math.test.ts +++ b/src/utils/math.test.ts @@ -12,6 +12,7 @@ import { reflect, degreeToRadians, vecToArray, + createRng, } from "./math"; describe("vec3", () => { @@ -138,6 +139,33 @@ describe("avg", () => { }); }); +describe("createRng", () => { + it("produces values in [0, 1)", () => { + const random = createRng(123); + for (let i = 0; i < 1000; i++) { + const v = random(); + expect(v).toBeGreaterThanOrEqual(0); + expect(v).toBeLessThan(1); + } + }); + + it("produces the same sequence for the same seed", () => { + const a = createRng(42); + const b = createRng(42); + for (let i = 0; i < 100; i++) { + expect(a()).toBe(b()); + } + }); + + it("produces different sequences for different seeds", () => { + const a = createRng(1); + const b = createRng(2); + const valuesA = Array.from({ length: 10 }, () => a()); + const valuesB = Array.from({ length: 10 }, () => b()); + expect(valuesA).not.toEqual(valuesB); + }); +}); + describe("reflect", () => { it("reflects off a flat surface", () => { const direction = vec3(1, -1, 0); diff --git a/src/utils/math.ts b/src/utils/math.ts index 9b9ed73..a815f0b 100644 --- a/src/utils/math.ts +++ b/src/utils/math.ts @@ -40,3 +40,14 @@ export const dot = (a: Vec3, b: Vec3): number => a.x * b.x + a.y * b.y + a.z * b export const normalize = (vec: Vec3): Vec3 => mul(vec, 1 / mag(vec)); export const reflect = (direction: Vec3, normal: Vec3) => sub(direction, mul(normal, dot(direction, normal) * 2)); + +// Mulberry32 +export const createRng = (seed: number): (() => number) => { + let s: number = seed; + return () => { + s = (s + 0x6d2b79f5) | 0; + let t = Math.imul(s ^ (s >>> 15), 1 | s); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +};