Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
Expand Down
Empty file.
154 changes: 154 additions & 0 deletions apps/typegpu-docs/src/examples/tests/perlin-cpu/index.ts
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions apps/typegpu-docs/src/examples/tests/perlin-cpu/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Perlin CPU",
"category": "tests",
"tags": ["test"],
"dev": true,
"coolFactor": 1
}
39 changes: 39 additions & 0 deletions packages/typegpu-noise/src/generator.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,6 +49,44 @@
};
})();

/**
* 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<vec3u>(value)', d.vec3u, 'runtime');

return {
seed3: tgpu.fn([d.vec3f])((value) => {

Check warning on line 74 in packages/typegpu-noise/src/generator.ts

View workflow job for this annotation

GitHub Actions / build-and-test

eslint(no-unused-vars)

Parameter 'value' is declared but never used. Unused parameters should start with a '_'.
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;

Expand Down
2 changes: 2 additions & 0 deletions packages/typegpu-noise/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 0 additions & 2 deletions packages/typegpu-noise/src/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
46 changes: 45 additions & 1 deletion packages/typegpu-noise/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { d } from 'typegpu';
import tgpu, { d, std } from 'typegpu';

export type Prettify<T> = {
[K in keyof T]: T[K];
Expand Down Expand Up @@ -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;
});
Loading