diff --git a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts index 9d3de4ec8e..ec5ae098ce 100644 --- a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts @@ -1,4 +1,4 @@ -import { perlin3d } from '@typegpu/noise'; +import { perlin3d, randomGeneratorSlot, XOROSHIRO64STARSTAR } from '@typegpu/noise'; import tgpu, { common, d } from 'typegpu'; import { abs, mix, mul, pow, sign, tanh } from 'typegpu/std'; import { defineControls } from '../../common/defineControls.ts'; @@ -36,25 +36,28 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const createRenderPipeline = (sharpenFn: (n: number, sharpness: number) => number) => - root.pipe(perlinCacheConfig.inject(dynamicLayout.$)).createRenderPipeline({ - vertex: common.fullScreenTriangle, - fragment: ({ uv }) => { - 'use gpu'; - const suv = mul(gridSize.$, uv); - const n = perlin3d.sample(d.vec3f(suv, time.$)); - - // Apply sharpening function - const sharp = sharpenFn(n, sharpness.$); - - // Map to 0-1 range - const n01 = sharp * 0.5 + 0.5; - - // Gradient map - const dark = d.vec3f(0, 0.2, 1); - const light = d.vec3f(1, 0.3, 0.5); - return d.vec4f(mix(dark, light, n01), 1); - }, - }); + root + .with(randomGeneratorSlot, XOROSHIRO64STARSTAR) + .pipe(perlinCacheConfig.inject(dynamicLayout.$)) + .createRenderPipeline({ + vertex: common.fullScreenTriangle, + fragment: ({ uv }) => { + 'use gpu'; + const suv = mul(gridSize.$, uv); + const n = perlin3d.sample(d.vec3f(suv, time.$)); + + // Apply sharpening function + const sharp = sharpenFn(n, sharpness.$); + + // Map to 0-1 range + const n01 = sharp * 0.5 + 0.5; + + // Gradient map + const dark = d.vec3f(0, 0.2, 1); + const light = d.vec3f(1, 0.3, 0.5); + return d.vec4f(mix(dark, light, n01), 1); + }, + }); const renderPipelines = { exponential: createRenderPipeline(exponentialSharpen), diff --git a/apps/typegpu-docs/src/examples/tests/perlin-cpu/index.html b/apps/typegpu-docs/src/examples/tests/perlin-cpu/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/typegpu-docs/src/examples/tests/perlin-cpu/index.ts b/apps/typegpu-docs/src/examples/tests/perlin-cpu/index.ts new file mode 100644 index 0000000000..d60ca6c1eb --- /dev/null +++ b/apps/typegpu-docs/src/examples/tests/perlin-cpu/index.ts @@ -0,0 +1,154 @@ +import tgpu, { d, std } from 'typegpu'; +import { perlin3d, randomGeneratorSlot, XOROSHIRO64STARSTAR } from '@typegpu/noise'; +import { cos, dot, floor, mix, sin, sqrt } from 'typegpu/std'; + +const root = await tgpu.init(); + +const TWO_PI = d.f32(d.f32(Math.PI) * d.f32(2)); +let seed: d.v2u; + +const rotl = (x: number, k: number) => { + return (x << k) | (x >>> (32 - k)); +}; + +const next = () => { + const s0 = seed[0]; + let s1 = seed[1]; + s1 ^= s0; + seed[0] = rotl(s0, 26) ^ s1 ^ (s1 << 9); + seed[1] = rotl(s1, 13); + const temp = Math.imul(seed[0], 0x9e3779bb); + return Math.imul(rotl(temp, 5), 5); +}; + +const hash = (value: number) => { + let x = value ^ (value >>> 17); + x = Math.imul(x, 0xed5ad4bb); + x = x ^ (x >>> 11); + x = Math.imul(x, 0xac4c1b51); + x = x ^ (x >>> 15); + x = Math.imul(x, 0x31848bab); + x = x ^ (x >>> 14); + return x; +}; + +function randSeed3(value: d.v3f) { + const dataView = new DataView(new ArrayBuffer(12)); + dataView.setFloat32(0, value.x, true); + dataView.setFloat32(4, value.y, true); + dataView.setFloat32(8, value.z, true); + const x = dataView.getUint32(0, true); + const y = dataView.getUint32(4, true); + const z = dataView.getUint32(8, true); + const hx = hash(x ^ 0x4ab57dfb); + const hy = hash(y ^ 0xacdeda47); + const hz = hash(z ^ 0xbca0294b); + seed = d.vec2u(hash(hx ^ rotl(hz, 16)), hash(rotl(hy, 16) ^ hz)); +} + +function randomGeneratorShell() { + const r = next(); + const mantissa = r & 0x007fffff; + const bits = 0x3f800000 | mantissa; + const dataView = new DataView(new ArrayBuffer(4)); + dataView.setUint32(0, bits, true); + return dataView.getFloat32(0, true) - 1; +} + +function randOnUnitSphere() { + const z = d.f32(d.f32(d.f32(2) * d.f32(randomGeneratorShell())) - d.f32(1)); + const oneMinusZSq = d.f32(sqrt(d.f32(d.f32(1) - d.f32(z * z)))); + const theta = d.f32(TWO_PI * d.f32(randomGeneratorShell())); + const x = d.f32(d.f32(cos(theta)) * oneMinusZSq); + const y = d.f32(d.f32(sin(theta)) * oneMinusZSq); + + return d.vec3f(x, y, z); +} + +export function computeJunctionGradient(pos: d.v3i) { + 'use gpu'; + randSeed3(0.001 * d.vec3f(pos)); + return randOnUnitSphere(); +} + +function dotProdGrid(pos: d.v3f, junction: d.v3f) { + 'use gpu'; + const relative = pos - junction; + const gridVector = computeJunctionGradient(d.vec3i(junction)); + return d.f32(dot(relative, gridVector)); +} + +function quinticInterpolation(t: d.v3f) { + 'use gpu'; + return t * t * t * (t * (t * 6 - 15) + 10); +} + +export function sample(pos: d.v3f) { + 'use gpu'; + const minJunction = floor(pos); + + const xyz = dotProdGrid(pos, minJunction); + const xyZ = dotProdGrid(pos, minJunction + d.vec3f(0, 0, 1)); + const xYz = dotProdGrid(pos, minJunction + d.vec3f(0, 1, 0)); + const xYZ = dotProdGrid(pos, minJunction + d.vec3f(0, 1, 1)); + const Xyz = dotProdGrid(pos, minJunction + d.vec3f(1, 0, 0)); + const XyZ = dotProdGrid(pos, minJunction + d.vec3f(1, 0, 1)); + const XYz = dotProdGrid(pos, minJunction + d.vec3f(1, 1, 0)); + const XYZ = dotProdGrid(pos, minJunction + d.vec3f(1, 1, 1)); + + const partial = pos - minJunction; + const smoothPartial = quinticInterpolation(partial); + + // Resolving the z-axis into a xy-slice + const xy = mix(xyz, xyZ, smoothPartial.z); + const xY = mix(xYz, xYZ, smoothPartial.z); + const Xy = mix(Xyz, XyZ, smoothPartial.z); + const XY = mix(XYz, XYZ, smoothPartial.z); + + // Merging the y-axis + const x = mix(xy, xY, smoothPartial.y); + const X = mix(Xy, XY, smoothPartial.y); + + return mix(x, X, smoothPartial.x); +} + +const SAMPLES = 100; + +const cpuBuffer = new Float32Array(SAMPLES); +for (let i = 1; i <= SAMPLES; i++) { + const pointInWorld = d.vec3f(i ** 2, i, 1 / i); + const direction = pointInWorld; + const normalizedDirection = std.normalize(direction); + + const perlinValue = sample(normalizedDirection); + cpuBuffer[i - 1] = perlinValue; +} + +const gpuBuffer = root.createMutable(d.arrayOf(d.f32, SAMPLES)); +const f = root.with(randomGeneratorSlot, XOROSHIRO64STARSTAR).createGuardedComputePipeline(() => { + 'use gpu'; + for (let i = 1; i <= SAMPLES; i++) { + const pointInWorld = d.vec3f(d.f32(i) ** 2, i, 1 / d.f32(i)); + const direction = pointInWorld; + const normalizedDirection = std.normalize(direction); + + const perlinValue = perlin3d.sample(normalizedDirection); + gpuBuffer.$[i - 1] = perlinValue; + } +}); +f.dispatchThreads(); +const gpuReadBuffer = new Float32Array(await gpuBuffer.read()); +for (let i = 1; i <= SAMPLES; i++) { + console.log( + 'cpu vs gpu perlin abs diff', + Math.abs(cpuBuffer[i - 1] - gpuReadBuffer[i - 1]).toFixed(8), // one more than f32 precision + ); +} + +// #region Example controls and cleanup + +export function onCleanup() { + root.destroy(); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/tests/perlin-cpu/meta.json b/apps/typegpu-docs/src/examples/tests/perlin-cpu/meta.json new file mode 100644 index 0000000000..0913fc703b --- /dev/null +++ b/apps/typegpu-docs/src/examples/tests/perlin-cpu/meta.json @@ -0,0 +1,7 @@ +{ + "title": "Perlin CPU", + "category": "tests", + "tags": ["test"], + "dev": true, + "coolFactor": 1 +} diff --git a/packages/typegpu-noise/src/generator.ts b/packages/typegpu-noise/src/generator.ts index fa9d1153ef..5b69cda76f 100644 --- a/packages/typegpu-noise/src/generator.ts +++ b/packages/typegpu-noise/src/generator.ts @@ -1,5 +1,6 @@ import tgpu, { d, type TgpuFnShell, type TgpuSlot } from 'typegpu'; import { cos, dot, fract } from 'typegpu/std'; +import { hash, rotl, u32To01F32 } from './utils.ts'; export interface StatefulGenerator { seed?: (seed: number) => void; @@ -48,6 +49,44 @@ export const BPETER: StatefulGenerator = (() => { }; })(); +/** + * Incorporated from https://github.com/chaos-matters/chaos-master + * by deluksic and Komediruzecki + */ +export const XOROSHIRO64STARSTAR: StatefulGenerator = (() => { + const seed = tgpu.privateVar(d.vec2u); + + const next = tgpu.fn( + [], + d.u32, + )(() => { + const s0 = seed.$[0]; + let s1 = seed.$[1]; + s1 ^= s0; + seed.$[0] = rotl(s0, 26) ^ s1 ^ (s1 << 9); + seed.$[1] = rotl(s1, 13); + return rotl(seed.$[0] * 0x9e3779bb, 5) * 5; + }); + + const bitcast = tgpu['~unstable'].rawCodeSnippet('bitcast(value)', d.vec3u, 'runtime'); + + return { + seed3: tgpu.fn([d.vec3f])((value) => { + const u32Value = bitcast.$; + const hx = hash(u32Value.x ^ 0x4ab57dfb); + const hy = hash(u32Value.y ^ 0xacdeda47); + const hz = hash(u32Value.z ^ 0xbca0294b); + seed.$ = d.vec2u(hash(hx ^ rotl(hz, 16)), hash(rotl(hy, 16) ^ hz)); + }), + + sample: randomGeneratorShell(() => { + 'use gpu'; + const r = next(); + return u32To01F32(r); + }).$name('sample'), + }; +})(); + // The default (Can change between releases to improve uniformity). export const DefaultGenerator: StatefulGenerator = BPETER; diff --git a/packages/typegpu-noise/src/index.ts b/packages/typegpu-noise/src/index.ts index 97dc584cf3..b750eee814 100644 --- a/packages/typegpu-noise/src/index.ts +++ b/packages/typegpu-noise/src/index.ts @@ -145,11 +145,13 @@ export { BPETER, // The default (Can change between releases to improve uniformity). DefaultGenerator, + XOROSHIRO64STARSTAR, // --- randomGeneratorShell, randomGeneratorSlot, type StatefulGenerator, } from './generator.ts'; +export { hash, u32To01F32, rotl } from './utils.ts'; export * as perlin2d from './perlin-2d/index.ts'; export * as perlin3d from './perlin-3d/index.ts'; diff --git a/packages/typegpu-noise/src/random.ts b/packages/typegpu-noise/src/random.ts index e1ddd472b2..dfbf0f4b4d 100644 --- a/packages/typegpu-noise/src/random.ts +++ b/packages/typegpu-noise/src/random.ts @@ -9,8 +9,6 @@ const warnIfNotProvided = tgpu.comptime((seedFnName: keyof typeof randomGenerato if (!randomGeneratorSlot.$[seedFnName]) { console.warn(`Called \`randf.${seedFnName}\`, but it wasn't provided`); } - - return undefined; }); export const randSeed = tgpu.fn([d.f32])((seed) => { diff --git a/packages/typegpu-noise/src/utils.ts b/packages/typegpu-noise/src/utils.ts index 9e90781916..c5223b12d3 100644 --- a/packages/typegpu-noise/src/utils.ts +++ b/packages/typegpu-noise/src/utils.ts @@ -1,4 +1,4 @@ -import type { d } from 'typegpu'; +import tgpu, { d, std } from 'typegpu'; export type Prettify = { [K in keyof T]: T[K]; @@ -28,3 +28,47 @@ export function quinticDerivative(t: d.vecBase): d.vecBase { 'use gpu'; return 30 * t * t * (t * (t - 2) + 1); } + +/** + * Left circular shif of x by k positions. + */ +export const rotl = tgpu.fn( + [d.u32, d.u32], + d.u32, +)((x, k) => { + return (x << k) | (x >> (32 - k)); +}); + +/** + * Converts `u32` to `f32` value in the range `[0.0, 1.0)`. + */ +export const u32To01F32 = tgpu.fn( + [d.u32], + d.f32, +)((value) => { + const mantissa = value & 0x007fffff; + const bits = 0x3f800000 | mantissa; + const f = std.bitcastU32toF32(bits); + return f - 1; +}); + +/** + * Simple hashing function to scramble the seed. + * Keep in mind that `hash(0) -> 0`. + * + * Incorporated from https://github.com/chaos-matters/chaos-master + * by deluksic and Komediruzecki + */ +export const hash = tgpu.fn( + [d.u32], + d.u32, +)((value) => { + let x = value ^ (value >> 17); + x *= d.u32(0xed5ad4bb); + x ^= x >> 11; + x *= d.u32(0xac4c1b51); + x ^= x >> 15; + x *= d.u32(0x31848bab); + x ^= x >> 14; + return x; +});