Skip to content

Commit a23dfe9

Browse files
committed
cpu perlin with xoroshiro
1 parent 08fb40f commit a23dfe9

8 files changed

Lines changed: 270 additions & 23 deletions

File tree

apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { perlin3d } from '@typegpu/noise';
1+
import { perlin3d, randomGeneratorSlot, XOROSHIRO64STARSTAR } from '@typegpu/noise';
22
import tgpu, { common, d } from 'typegpu';
33
import { abs, mix, mul, pow, sign, tanh } from 'typegpu/std';
44
import { defineControls } from '../../common/defineControls.ts';
@@ -36,25 +36,28 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement;
3636
const context = root.configureContext({ canvas, alphaMode: 'premultiplied' });
3737

3838
const createRenderPipeline = (sharpenFn: (n: number, sharpness: number) => number) =>
39-
root.pipe(perlinCacheConfig.inject(dynamicLayout.$)).createRenderPipeline({
40-
vertex: common.fullScreenTriangle,
41-
fragment: ({ uv }) => {
42-
'use gpu';
43-
const suv = mul(gridSize.$, uv);
44-
const n = perlin3d.sample(d.vec3f(suv, time.$));
45-
46-
// Apply sharpening function
47-
const sharp = sharpenFn(n, sharpness.$);
48-
49-
// Map to 0-1 range
50-
const n01 = sharp * 0.5 + 0.5;
51-
52-
// Gradient map
53-
const dark = d.vec3f(0, 0.2, 1);
54-
const light = d.vec3f(1, 0.3, 0.5);
55-
return d.vec4f(mix(dark, light, n01), 1);
56-
},
57-
});
39+
root
40+
.with(randomGeneratorSlot, XOROSHIRO64STARSTAR)
41+
.pipe(perlinCacheConfig.inject(dynamicLayout.$))
42+
.createRenderPipeline({
43+
vertex: common.fullScreenTriangle,
44+
fragment: ({ uv }) => {
45+
'use gpu';
46+
const suv = mul(gridSize.$, uv);
47+
const n = perlin3d.sample(d.vec3f(suv, time.$));
48+
49+
// Apply sharpening function
50+
const sharp = sharpenFn(n, sharpness.$);
51+
52+
// Map to 0-1 range
53+
const n01 = sharp * 0.5 + 0.5;
54+
55+
// Gradient map
56+
const dark = d.vec3f(0, 0.2, 1);
57+
const light = d.vec3f(1, 0.3, 0.5);
58+
return d.vec4f(mix(dark, light, n01), 1);
59+
},
60+
});
5861

5962
const renderPipelines = {
6063
exponential: createRenderPipeline(exponentialSharpen),

apps/typegpu-docs/src/examples/tests/perlin-cpu/index.html

Whitespace-only changes.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import tgpu, { d, std } from 'typegpu';
2+
import { perlin3d, randomGeneratorSlot, XOROSHIRO64STARSTAR } from '@typegpu/noise';
3+
import { cos, dot, floor, mix, sin, sqrt } from 'typegpu/std';
4+
5+
const root = await tgpu.init();
6+
7+
const TWO_PI = d.f32(d.f32(Math.PI) * d.f32(2));
8+
let seed: d.v2u;
9+
10+
const rotl = (x: number, k: number) => {
11+
return (x << k) | (x >>> (32 - k));
12+
};
13+
14+
const next = () => {
15+
const s0 = seed[0];
16+
let s1 = seed[1];
17+
s1 ^= s0;
18+
seed[0] = rotl(s0, 26) ^ s1 ^ (s1 << 9);
19+
seed[1] = rotl(s1, 13);
20+
const temp = Math.imul(seed[0], 0x9e3779bb);
21+
return Math.imul(rotl(temp, 5), 5);
22+
};
23+
24+
const hash = (value: number) => {
25+
let x = value ^ (value >>> 17);
26+
x = Math.imul(x, 0xed5ad4bb);
27+
x = x ^ (x >>> 11);
28+
x = Math.imul(x, 0xac4c1b51);
29+
x = x ^ (x >>> 15);
30+
x = Math.imul(x, 0x31848bab);
31+
x = x ^ (x >>> 14);
32+
return x;
33+
};
34+
35+
function randSeed3(value: d.v3f) {
36+
const dataView = new DataView(new ArrayBuffer(12));
37+
dataView.setFloat32(0, value.x, true);
38+
dataView.setFloat32(4, value.y, true);
39+
dataView.setFloat32(8, value.z, true);
40+
const x = dataView.getUint32(0, true);
41+
const y = dataView.getUint32(4, true);
42+
const z = dataView.getUint32(8, true);
43+
const hx = hash(x ^ 0x4ab57dfb);
44+
const hy = hash(y ^ 0xacdeda47);
45+
const hz = hash(z ^ 0xbca0294b);
46+
seed = d.vec2u(hash(hx ^ rotl(hz, 16)), hash(rotl(hy, 16) ^ hz));
47+
}
48+
49+
function randomGeneratorShell() {
50+
const r = next();
51+
const mantissa = r & 0x007fffff;
52+
const bits = 0x3f800000 | mantissa;
53+
const dataView = new DataView(new ArrayBuffer(4));
54+
dataView.setUint32(0, bits, true);
55+
return dataView.getFloat32(0, true) - 1;
56+
}
57+
58+
function randOnUnitSphere() {
59+
const z = d.f32(d.f32(d.f32(2) * d.f32(randomGeneratorShell())) - d.f32(1));
60+
const oneMinusZSq = d.f32(sqrt(d.f32(d.f32(1) - d.f32(z * z))));
61+
const theta = d.f32(TWO_PI * d.f32(randomGeneratorShell()));
62+
const x = d.f32(d.f32(cos(theta)) * oneMinusZSq);
63+
const y = d.f32(d.f32(sin(theta)) * oneMinusZSq);
64+
65+
return d.vec3f(x, y, z);
66+
}
67+
68+
export function computeJunctionGradient(pos: d.v3i) {
69+
'use gpu';
70+
randSeed3(0.001 * d.vec3f(pos));
71+
return randOnUnitSphere();
72+
}
73+
74+
function dotProdGrid(pos: d.v3f, junction: d.v3f) {
75+
'use gpu';
76+
const relative = pos - junction;
77+
const gridVector = computeJunctionGradient(d.vec3i(junction));
78+
return d.f32(dot(relative, gridVector));
79+
}
80+
81+
function quinticInterpolation(t: d.v3f) {
82+
'use gpu';
83+
return t * t * t * (t * (t * 6 - 15) + 10);
84+
}
85+
86+
export function sample(pos: d.v3f) {
87+
'use gpu';
88+
const minJunction = floor(pos);
89+
90+
const xyz = dotProdGrid(pos, minJunction);
91+
const xyZ = dotProdGrid(pos, minJunction + d.vec3f(0, 0, 1));
92+
const xYz = dotProdGrid(pos, minJunction + d.vec3f(0, 1, 0));
93+
const xYZ = dotProdGrid(pos, minJunction + d.vec3f(0, 1, 1));
94+
const Xyz = dotProdGrid(pos, minJunction + d.vec3f(1, 0, 0));
95+
const XyZ = dotProdGrid(pos, minJunction + d.vec3f(1, 0, 1));
96+
const XYz = dotProdGrid(pos, minJunction + d.vec3f(1, 1, 0));
97+
const XYZ = dotProdGrid(pos, minJunction + d.vec3f(1, 1, 1));
98+
99+
const partial = pos - minJunction;
100+
const smoothPartial = quinticInterpolation(partial);
101+
102+
// Resolving the z-axis into a xy-slice
103+
const xy = mix(xyz, xyZ, smoothPartial.z);
104+
const xY = mix(xYz, xYZ, smoothPartial.z);
105+
const Xy = mix(Xyz, XyZ, smoothPartial.z);
106+
const XY = mix(XYz, XYZ, smoothPartial.z);
107+
108+
// Merging the y-axis
109+
const x = mix(xy, xY, smoothPartial.y);
110+
const X = mix(Xy, XY, smoothPartial.y);
111+
112+
return mix(x, X, smoothPartial.x);
113+
}
114+
115+
const SAMPLES = 100;
116+
117+
const cpuBuffer = new Float32Array(SAMPLES);
118+
for (let i = 1; i <= SAMPLES; i++) {
119+
const pointInWorld = d.vec3f(i ** 2, i, 1 / i);
120+
const direction = pointInWorld;
121+
const normalizedDirection = std.normalize(direction);
122+
123+
const perlinValue = sample(normalizedDirection);
124+
cpuBuffer[i - 1] = perlinValue;
125+
}
126+
127+
const gpuBuffer = root.createMutable(d.arrayOf(d.f32, SAMPLES));
128+
const f = root.with(randomGeneratorSlot, XOROSHIRO64STARSTAR).createGuardedComputePipeline(() => {
129+
'use gpu';
130+
for (let i = 1; i <= SAMPLES; i++) {
131+
const pointInWorld = d.vec3f(d.f32(i) ** 2, i, 1 / d.f32(i));
132+
const direction = pointInWorld;
133+
const normalizedDirection = std.normalize(direction);
134+
135+
const perlinValue = perlin3d.sample(normalizedDirection);
136+
gpuBuffer.$[i - 1] = perlinValue;
137+
}
138+
});
139+
f.dispatchThreads();
140+
const gpuReadBuffer = new Float32Array(await gpuBuffer.read());
141+
for (let i = 1; i <= SAMPLES; i++) {
142+
console.log(
143+
'cpu vs gpu perlin abs diff',
144+
Math.abs(cpuBuffer[i - 1] - gpuReadBuffer[i - 1]).toFixed(8), // one more than f32 precision
145+
);
146+
}
147+
148+
// #region Example controls and cleanup
149+
150+
export function onCleanup() {
151+
root.destroy();
152+
}
153+
154+
// #endregion
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"title": "Perlin CPU",
3+
"category": "tests",
4+
"tags": ["test"],
5+
"dev": true,
6+
"coolFactor": 1
7+
}

packages/typegpu-noise/src/generator.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import tgpu, { d, type TgpuFnShell, type TgpuSlot } from 'typegpu';
22
import { cos, dot, fract } from 'typegpu/std';
3+
import { hash, rotl, u32To01F32 } from './utils.ts';
34

45
export interface StatefulGenerator {
56
seed?: (seed: number) => void;
@@ -48,6 +49,44 @@ export const BPETER: StatefulGenerator = (() => {
4849
};
4950
})();
5051

52+
/**
53+
* Incorporated from https://github.com/chaos-matters/chaos-master
54+
* by deluksic and Komediruzecki
55+
*/
56+
export const XOROSHIRO64STARSTAR: StatefulGenerator = (() => {
57+
const seed = tgpu.privateVar(d.vec2u);
58+
59+
const next = tgpu.fn(
60+
[],
61+
d.u32,
62+
)(() => {
63+
const s0 = seed.$[0];
64+
let s1 = seed.$[1];
65+
s1 ^= s0;
66+
seed.$[0] = rotl(s0, 26) ^ s1 ^ (s1 << 9);
67+
seed.$[1] = rotl(s1, 13);
68+
return rotl(seed.$[0] * 0x9e3779bb, 5) * 5;
69+
});
70+
71+
const bitcast = tgpu['~unstable'].rawCodeSnippet('bitcast<vec3u>(value)', d.vec3u, 'runtime');
72+
73+
return {
74+
seed3: tgpu.fn([d.vec3f])((value) => {
75+
const u32Value = bitcast.$;
76+
const hx = hash(u32Value.x ^ 0x4ab57dfb);
77+
const hy = hash(u32Value.y ^ 0xacdeda47);
78+
const hz = hash(u32Value.z ^ 0xbca0294b);
79+
seed.$ = d.vec2u(hash(hx ^ rotl(hz, 16)), hash(rotl(hy, 16) ^ hz));
80+
}),
81+
82+
sample: randomGeneratorShell(() => {
83+
'use gpu';
84+
const r = next();
85+
return u32To01F32(r);
86+
}).$name('sample'),
87+
};
88+
})();
89+
5190
// The default (Can change between releases to improve uniformity).
5291
export const DefaultGenerator: StatefulGenerator = BPETER;
5392

packages/typegpu-noise/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,13 @@ export {
145145
BPETER,
146146
// The default (Can change between releases to improve uniformity).
147147
DefaultGenerator,
148+
XOROSHIRO64STARSTAR,
148149
// ---
149150
randomGeneratorShell,
150151
randomGeneratorSlot,
151152
type StatefulGenerator,
152153
} from './generator.ts';
153154

155+
export { hash, u32To01F32, rotl } from './utils.ts';
154156
export * as perlin2d from './perlin-2d/index.ts';
155157
export * as perlin3d from './perlin-3d/index.ts';

packages/typegpu-noise/src/random.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const warnIfNotProvided = tgpu.comptime((seedFnName: keyof typeof randomGenerato
99
if (!randomGeneratorSlot.$[seedFnName]) {
1010
console.warn(`Called \`randf.${seedFnName}\`, but it wasn't provided`);
1111
}
12-
13-
return undefined;
1412
});
1513

1614
export const randSeed = tgpu.fn([d.f32])((seed) => {

packages/typegpu-noise/src/utils.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { d } from 'typegpu';
1+
import tgpu, { d, std } from 'typegpu';
22

33
export type Prettify<T> = {
44
[K in keyof T]: T[K];
@@ -28,3 +28,47 @@ export function quinticDerivative(t: d.vecBase): d.vecBase {
2828
'use gpu';
2929
return 30 * t * t * (t * (t - 2) + 1);
3030
}
31+
32+
/**
33+
* Left circular shif of x by k positions.
34+
*/
35+
export const rotl = tgpu.fn(
36+
[d.u32, d.u32],
37+
d.u32,
38+
)((x, k) => {
39+
return (x << k) | (x >> (32 - k));
40+
});
41+
42+
/**
43+
* Converts `u32` to `f32` value in the range `[0.0, 1.0)`.
44+
*/
45+
export const u32To01F32 = tgpu.fn(
46+
[d.u32],
47+
d.f32,
48+
)((value) => {
49+
const mantissa = value & 0x007fffff;
50+
const bits = 0x3f800000 | mantissa;
51+
const f = std.bitcastU32toF32(bits);
52+
return f - 1;
53+
});
54+
55+
/**
56+
* Simple hashing function to scramble the seed.
57+
* Keep in mind that `hash(0) -> 0`.
58+
*
59+
* Incorporated from https://github.com/chaos-matters/chaos-master
60+
* by deluksic and Komediruzecki
61+
*/
62+
export const hash = tgpu.fn(
63+
[d.u32],
64+
d.u32,
65+
)((value) => {
66+
let x = value ^ (value >> 17);
67+
x *= d.u32(0xed5ad4bb);
68+
x ^= x >> 11;
69+
x *= d.u32(0xac4c1b51);
70+
x ^= x >> 15;
71+
x *= d.u32(0x31848bab);
72+
x ^= x >> 14;
73+
return x;
74+
});

0 commit comments

Comments
 (0)