Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions apps/react-prerendering-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.next
next-env.d.ts
11 changes: 11 additions & 0 deletions apps/react-prerendering-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# React Prerendering Test

Minimal Next.js app for testing how `@typegpu/react` behaves during prerendering.

## Usage

`pnpm check-ssr` runs build with both SSR enabled and disabled

## Dev

SSR is disabled with the `NEXT_PUBLIC_DISABLE_SSR` env var.
16 changes: 16 additions & 0 deletions apps/react-prerendering-test/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions apps/react-prerendering-test/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ReactNode } from 'react';
import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'React Prerendering Test',
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body
style={{
margin: 0,
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
background: '#171724',
}}
>
{children}
</body>
</html>
);
}
12 changes: 12 additions & 0 deletions apps/react-prerendering-test/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import dynamic from 'next/dynamic';
import Shader from '../components/Shader.tsx';

const ShaderNoSSR = dynamic(() => import('../components/Shader.tsx'), {
ssr: false,
});

export default function Page() {
return process.env.NEXT_PUBLIC_DISABLE_SSR ? <ShaderNoSSR /> : <Shader />;
}
Comment thread
cieplypolar marked this conversation as resolved.
7 changes: 7 additions & 0 deletions apps/react-prerendering-test/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = (api) => {
api.cache(true);
return {
presets: ['next/babel'],
plugins: ['./node_modules/unplugin-typegpu/dist/babel.js'], // cannot parse .ts file
};
Comment thread
cieplypolar marked this conversation as resolved.
};
22 changes: 22 additions & 0 deletions apps/react-prerendering-test/checkSsr.mts
Comment thread
cieplypolar marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { spawnSync } from 'node:child_process';

type Env = Record<string, string>;

function build(env: Env) {
const { status, stdout, stderr } = spawnSync('pnpm', ['next', 'build'], {
cwd: import.meta.dirname,
env: { ...process.env, ...env },
encoding: 'utf8',
});
return { failed: status !== 0, output: `stdout:\n${stdout}\nstderr:\n${stderr}` };
}

console.log('Building with SSR...');
const ssr = build({});
console.log(ssr.failed ? '\n' + ssr.output : 'OK');

console.log('========================================\n');

console.log('Building with SSR disabled...');
const noSsr = build({ NEXT_PUBLIC_DISABLE_SSR: '1' });
console.log(noSsr.failed ? noSsr.output : 'OK');
114 changes: 114 additions & 0 deletions apps/react-prerendering-test/components/Shader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client';

import { useConfigureContext, useFrame, useRoot, useUniform } from '@typegpu/react';
import { useMemo } from 'react';
import tgpu, { common, d, std } from 'typegpu';
import { perlin2d } from '@typegpu/noise';
import { hexToOklab, oklabToRgb } from '@typegpu/color';

export default function Shader() {
const root = useRoot();
const time = useUniform(d.f32);

function noise(v: d.v2f) {
Comment thread
cieplypolar marked this conversation as resolved.
Outdated
'use gpu';
return perlin2d.sample(v);
}

const octavesAccessor = tgpu.accessor(d.u32);
const standardizeNoiseAccessor = tgpu.accessor(d.bool);
const getMaxValue = tgpu.comptime((octaves: number) => 1 - 2 ** -octaves);
const rotation = d.mat2x2f(0.8, 0.6, -0.6, 0.8);

function fbm(v: d.v2f) {
'use gpu';
let u = d.vec2f(v);
let f = d.f32();

// first octave
{
let sample = noise(u + time.$);
if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
f += 0.5 * sample;
u = rotation * u * 2.01;
}

for (const i of tgpu.unroll(std.range(2, octavesAccessor.$))) {
let sample = noise(u);
if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
f += 0.5 ** i * sample;
u = rotation * u * (2 + i / 100);
}

// last octave
{
let sample = noise(u + std.sin(time.$));
if (standardizeNoiseAccessor.$) sample = sample * 0.5 + 0.5;
f += 0.5 ** d.f32(octavesAccessor.$) * sample;
u = rotation * u * 2.01;
}

return f / getMaxValue(octavesAccessor.$);
}

const fbm4 = tgpu.fn(fbm).with(octavesAccessor, 4).with(standardizeNoiseAccessor, false);
const fbm6 = tgpu.fn(fbm).with(octavesAccessor, 6).with(standardizeNoiseAccessor, true);

function domainWarp(v: d.v2f) {
'use gpu';
return fbm6(v + fbm4(v + fbm4(v)));
}

function palette(t: number) {
'use gpu';
const purple = hexToOklab('#c04bf2');
const blue = hexToOklab('#4e65f6');
const dark = hexToOklab('#0f092b');

const factor1 = std.smoothstep(0.25, 0.5, t);
const factor2 = std.smoothstep(0.5, 0.7, t);

const mixed = std.mix(std.mix(dark, blue, factor1), purple, factor2);
return oklabToRgb(mixed);
}

const renderPipeline = useMemo(
() =>
root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: ({ uv }) => {
'use gpu';
const centeredUV = (2 * uv - 1) * 4;

const sample = domainWarp(centeredUV);
const sampleRight = domainWarp(centeredUV + d.vec2f(0.04, 0));
const sampleDown = domainWarp(centeredUV + d.vec2f(0, 0.04));

const dx = d.vec3f(0.04, 0, (sampleRight - sample) * 0.4);
const dy = d.vec3f(0, 0.04, (sampleDown - sample) * 0.4);
const normal = std.normalize(std.cross(dx, dy));

const lightDir = std.normalize(d.vec3f(0.0, -1.0, 1.0));
const diffuse = std.max(0.0, std.dot(normal, lightDir));

const baseColor = palette(sample);

const litColor = baseColor * diffuse;

return d.vec4f(litColor, 1);
},
}),
[root, time],
);

const { ref, ctxRef } = useConfigureContext({ autoResize: true, alphaMode: 'premultiplied' });

useFrame(({ elapsedSeconds }) => {
if (!ctxRef.current) return;

time.write(elapsedSeconds * 0.5);
renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3);
});

return <canvas ref={ref} style={{ display: 'block', width: '80vmin', height: '80vmin' }} />;
}
8 changes: 8 additions & 0 deletions apps/react-prerendering-test/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
output: 'export',
distDir: './dist',
};

export default nextConfig;
30 changes: 30 additions & 0 deletions apps/react-prerendering-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "react-prerendering-test",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"dev:no-ssr": "NEXT_PUBLIC_DISABLE_SSR=1 next dev",
"build:no-ssr": "NEXT_PUBLIC_DISABLE_SSR=1 next build",
"check-ssr": "node checkSsr.mts"
Comment thread
cieplypolar marked this conversation as resolved.
},
"dependencies": {
"@typegpu/color": "workspace:*",
"@typegpu/noise": "workspace:*",
"@typegpu/react": "workspace:*",
"next": "^16.2.7",
"react": "catalog:",
"react-dom": "catalog:",
"typegpu": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:types",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@webgpu/types": "catalog:types",
"typescript": "catalog:types",
"unplugin-typegpu": "workspace:*"
}
}
37 changes: 37 additions & 0 deletions apps/react-prerendering-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"allowJs": true,
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"types": ["@webgpu/types", "@types/react", "@types/react-dom", "@types/node"]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts",
"dist/types/**/*.ts",
"dist/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
Loading
Loading